import logging
from collections import OrderedDict

import netaddr

from ryu.services.protocols.bgp.base import SUPPORTED_GLOBAL_RF
from ryu.services.protocols.bgp.info_base.rtc import RtcTable
from ryu.services.protocols.bgp.info_base.ipv4 import Ipv4Path
from ryu.services.protocols.bgp.info_base.ipv4 import Ipv4Table
from ryu.services.protocols.bgp.info_base.ipv6 import Ipv6Path
from ryu.services.protocols.bgp.info_base.ipv6 import Ipv6Table
from ryu.services.protocols.bgp.info_base.vpnv4 import Vpnv4Table
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Table
from ryu.services.protocols.bgp.info_base.vrf4 import Vrf4Table
from ryu.services.protocols.bgp.info_base.vrf6 import Vrf6Table
from ryu.services.protocols.bgp.info_base.vrfevpn import VrfEvpnTable
from ryu.services.protocols.bgp.info_base.evpn import EvpnTable
from ryu.services.protocols.bgp.info_base.ipv4fs import IPv4FlowSpecPath
from ryu.services.protocols.bgp.info_base.ipv4fs import IPv4FlowSpecTable
from ryu.services.protocols.bgp.info_base.vpnv4fs import VPNv4FlowSpecTable
from ryu.services.protocols.bgp.info_base.vrf4fs import Vrf4FlowSpecTable
from ryu.services.protocols.bgp.info_base.ipv6fs import IPv6FlowSpecPath
from ryu.services.protocols.bgp.info_base.ipv6fs import IPv6FlowSpecTable
from ryu.services.protocols.bgp.info_base.vpnv6fs import VPNv6FlowSpecTable
from ryu.services.protocols.bgp.info_base.vrf6fs import Vrf6FlowSpecTable
from ryu.services.protocols.bgp.info_base.l2vpnfs import L2VPNFlowSpecTable
from ryu.services.protocols.bgp.info_base.vrfl2vpnfs import L2vpnFlowSpecPath
from ryu.services.protocols.bgp.info_base.vrfl2vpnfs import L2vpnFlowSpecTable
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV6
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_L2_EVPN
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4_FLOWSPEC
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV6_FLOWSPEC
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_L2VPN_FLOWSPEC
from ryu.services.protocols.bgp.rtconf.vrfs import SUPPORTED_VRF_RF
from ryu.services.protocols.bgp.utils.bgp import create_v4flowspec_actions
from ryu.services.protocols.bgp.utils.bgp import create_v6flowspec_actions
from ryu.services.protocols.bgp.utils.bgp import create_l2vpnflowspec_actions

from ryu.lib import type_desc
from ryu.lib import ip
from ryu.lib.packet.bgp import RF_IPv4_UC
from ryu.lib.packet.bgp import RF_IPv6_UC
from ryu.lib.packet.bgp import RF_IPv4_VPN
from ryu.lib.packet.bgp import RF_IPv6_VPN
from ryu.lib.packet.bgp import RF_L2_EVPN
from ryu.lib.packet.bgp import RF_IPv4_FLOWSPEC
from ryu.lib.packet.bgp import RF_IPv6_FLOWSPEC
from ryu.lib.packet.bgp import RF_VPNv4_FLOWSPEC
from ryu.lib.packet.bgp import RF_VPNv6_FLOWSPEC
from ryu.lib.packet.bgp import RF_L2VPN_FLOWSPEC
from ryu.lib.packet.bgp import RF_RTC_UC
from ryu.lib.packet.bgp import BGPPathAttributeOrigin
from ryu.lib.packet.bgp import BGPPathAttributeAsPath
from ryu.lib.packet.bgp import BGPPathAttributeExtendedCommunities
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_ORIGIN
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_AS_PATH
from ryu.lib.packet.bgp import BGP_ATTR_ORIGIN_IGP
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_EXTENDED_COMMUNITIES
from ryu.lib.packet.bgp import EvpnEsi
from ryu.lib.packet.bgp import EvpnArbitraryEsi
from ryu.lib.packet.bgp import EvpnNLRI
from ryu.lib.packet.bgp import EvpnMacIPAdvertisementNLRI
from ryu.lib.packet.bgp import EvpnInclusiveMulticastEthernetTagNLRI
from ryu.lib.packet.bgp import IPAddrPrefix
from ryu.lib.packet.bgp import IP6AddrPrefix
from ryu.lib.packet.bgp import FlowSpecIPv4NLRI
from ryu.lib.packet.bgp import FlowSpecIPv6NLRI
from ryu.lib.packet.bgp import FlowSpecL2VPNNLRI

from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4_prefix
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv6
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv6_prefix


LOG = logging.getLogger('bgpspeaker.core_managers.table_mixin')


class TableCoreManager(object):
    """Methods performing core operations on tables."""

    def __init__(self, core_service, common_conf):

        self._tables = {}
        self._rt_mgr = core_service.rt_manager
        self._signal_bus = core_service.signal_bus

        # (VRF) Tables to which the routes with a given route target
        # should be imported.
        #
        # Key: RouteTarget
        # Value: List of tables.
        self._tables_for_rt = {}

        # Global/Default tables, keyed by RouteFamily.
        self._global_tables = {}

        self._core_service = core_service
        self._signal_bus = self._core_service.signal_bus

        # VPN label range
        self._asbr_label_range = common_conf.label_range

        self._next_vpnv4_label = int(self._asbr_label_range[0])

        self._next_hop_label = {}

    @property
    def global_tables(self):
        return self._global_tables

    def remove_vrf_by_vrf_conf(self, vrf_conf):

        route_family = vrf_conf.route_family
        assert route_family in SUPPORTED_VRF_RF
        table_id = (vrf_conf.route_dist, route_family)

        vrf_table = self._tables.pop(table_id)

        self._remove_links_to_vrf_table(vrf_table)

        # Withdraw the best-path whose source was NC since it may have been
        # exported to VPN table.
        for destination in vrf_table.values():
            best_path = destination.best_path
            if best_path and best_path.source is None:
                vpn_clone = best_path.clone_to_vpn(vrf_conf.route_dist,
                                                   for_withdrawal=True)
                self.learn_path(vpn_clone)
        LOG.debug('VRF with RD %s marked for removal', vrf_conf.route_dist)

    def import_all_vpn_paths_to_vrf(self, vrf_table, import_rts=None):
        """Imports VPNv4/6 or EVPN paths from Global/VPN table into given
        VRFv4/6  or VRFEVPN table.
        :param vrf_table: Vrf table to which we import
        :type vrf_table: VrfTable
        :param import_rts: import RTs to override default import_rts of
         vrf table for this import
        :type import_rts: set of strings

        Checks if we have any path RT common with VRF table's import RT.
        """
        if vrf_table.route_family == Vrf4Table.ROUTE_FAMILY:
            vpn_table = self.get_vpn4_table()
        elif vrf_table.route_family == Vrf6Table.ROUTE_FAMILY:
            vpn_table = self.get_vpn6_table()
        elif vrf_table.route_family == VrfEvpnTable.ROUTE_FAMILY:
            vpn_table = self.get_evpn_table()
        elif vrf_table.route_family == Vrf4FlowSpecTable.ROUTE_FAMILY:
            vpn_table = self.get_vpnv4fs_table()
        elif vrf_table.route_family == Vrf6FlowSpecTable.ROUTE_FAMILY:
            vpn_table = self.get_vpnv6fs_table()
        elif vrf_table.route_family == L2vpnFlowSpecTable.ROUTE_FAMILY:
            vpn_table = self.get_l2vpnfs_table()
        else:
            raise ValueError('Invalid VRF table route family: %s' %
                             vrf_table.route_family)

        vrf_table.import_vpn_paths_from_table(vpn_table, import_rts)

    def learn_path(self, path):
        """Inserts `path` into correct global table.

        Since known paths to `Destination` has changes, we queue it for further
        processing.
        """
        # Get VPN/Global table
        table = self.get_global_table_by_route_family(path.route_family)
        gpath_dest = table.insert(path)
        # Since destination was updated, we enqueue it for processing.
        self._signal_bus.dest_changed(gpath_dest)

    def remember_sent_route(self, sent_route):
        """Records `sent_route` inside proper table.

        Records of `sent_route` from Adj-RIB-out.
        """
        route_family = sent_route.path.route_family
        table = self.get_global_table_by_route_family(route_family)
        table.insert_sent_route(sent_route)

    def on_interesting_rts_change(self, new_global_rts, removed_global_rts):
        """Update global tables as interested RTs changed.

        Adds `new_rts` and removes `removed_rts` rt nlris. Does not check if
        `new_rts` or `removed_rts` are already present. Schedules refresh
        request to peers that do not participate in RTC address-family.
        """
        # We add new RT NLRI and request RR for other peers.
        if new_global_rts:
            LOG.debug(
                'Sending route_refresh to all neighbors that'
                ' did not negotiate RTC capability.'
            )

            pm = self._core_service.peer_manager
            pm.schedule_rr_to_non_rtc_peers()
        if removed_global_rts:
            LOG.debug(
                'Cleaning up global tables as some interested RTs were removed'
            )
            self._clean_global_uninteresting_paths()

    def get_global_table_by_route_family(self, route_family):
        if route_family not in SUPPORTED_GLOBAL_RF:
            raise ValueError(
                'Given route family: %s currently not supported' % route_family
            )

        global_table = None
        if route_family == RF_IPv4_UC:
            global_table = self.get_ipv4_table()
        elif route_family == RF_IPv6_UC:
            global_table = self.get_ipv6_table()
        elif route_family == RF_IPv4_VPN:
            global_table = self.get_vpn4_table()
        elif route_family == RF_IPv6_VPN:
            global_table = self.get_vpn6_table()
        elif route_family == RF_L2_EVPN:
            global_table = self.get_evpn_table()
        elif route_family == RF_IPv4_FLOWSPEC:
            global_table = self.get_ipv4fs_table()
        elif route_family == RF_IPv6_FLOWSPEC:
            global_table = self.get_ipv6fs_table()
        elif route_family == RF_VPNv4_FLOWSPEC:
            global_table = self.get_vpnv4fs_table()
        elif route_family == RF_VPNv6_FLOWSPEC:
            global_table = self.get_vpnv6fs_table()
        elif route_family == RF_L2VPN_FLOWSPEC:
            global_table = self.get_l2vpnfs_table()
        elif route_family == RF_RTC_UC:
            global_table = self.get_rtc_table()

        return global_table

    def get_vrf_table(self, vrf_rd, vrf_rf):
        assert vrf_rd is not None
        return self._tables.get((vrf_rd, vrf_rf))

    def get_vrf_tables(self, vrf_rf=None):
        vrf_tables = {}
        for (scope_id, table_id), table in self._tables.items():
            if scope_id is None:
                continue
            if vrf_rf is not None and table_id != vrf_rf:
                continue
            vrf_tables[(scope_id, table_id)] = table
        return vrf_tables

    def get_ipv4_table(self):
        """Returns global IPv4 table.

        Creates the table if it does not exist.
        """

        vpn_table = self._global_tables.get(RF_IPv4_UC)
        # Lazy initialize the table.
        if not vpn_table:
            vpn_table = Ipv4Table(self._core_service, self._signal_bus)
            self._global_tables[RF_IPv4_UC] = vpn_table
            self._tables[(None, RF_IPv4_UC)] = vpn_table

        return vpn_table

    def get_ipv6_table(self):
        table = self._global_tables.get(RF_IPv6_UC)
        if not table:
            table = Ipv6Table(self._core_service, self._signal_bus)
            self._global_tables[RF_IPv6_UC] = table
            self._tables[(None, RF_IPv6_UC)] = table
        return table

    def get_vpn6_table(self):
        """Returns global VPNv6 table.

        Creates the table if it does not exist.
        """
        vpn_table = self._global_tables.get(RF_IPv6_VPN)
        # Lazy initialize the table.
        if not vpn_table:
            vpn_table = Vpnv6Table(self._core_service, self._signal_bus)
            self._global_tables[RF_IPv6_VPN] = vpn_table
            self._tables[(None, RF_IPv6_VPN)] = vpn_table

        return vpn_table

    def get_vpn4_table(self):
        """Returns global VPNv6 table.

        Creates the table if it does not exist.
        """
        vpn_table = self._global_tables.get(RF_IPv4_VPN)
        # Lazy initialize the table.
        if not vpn_table:
            vpn_table = Vpnv4Table(self._core_service, self._signal_bus)
            self._global_tables[RF_IPv4_VPN] = vpn_table
            self._tables[(None, RF_IPv4_VPN)] = vpn_table

        return vpn_table

    def get_evpn_table(self):
        """Returns global EVPN table.

        Creates the table if it does not exist.
        """
        evpn_table = self._global_tables.get(RF_L2_EVPN)
        # Lazy initialization of the table.
        if not evpn_table:
            evpn_table = EvpnTable(self._core_service, self._signal_bus)
            self._global_tables[RF_L2_EVPN] = evpn_table
            self._tables[(None, RF_L2_EVPN)] = evpn_table

        return evpn_table

    def get_rtc_table(self):
        """Returns global RTC table.

        Creates the table if it does not exist.
        """
        rtc_table = self._global_tables.get(RF_RTC_UC)
        # Lazy initialization of the table.
        if not rtc_table:
            rtc_table = RtcTable(self._core_service, self._signal_bus)
            self._global_tables[RF_RTC_UC] = rtc_table
            self._tables[(None, RF_RTC_UC)] = rtc_table
        return rtc_table

    def get_next_vpnv4_label(self):
        # Get next available label
        lbl = self._next_vpnv4_label
        # Check if label is within max. range allowed.
        if lbl > int(self._asbr_label_range[1]):
            # Currently we log error message if we exceed configured range.
            message = 'Have reached max label range'
            LOG.error(message)
            raise ValueError(message)
            # Increment label by 1 as next label.
        self._next_vpnv4_label += 1
        return lbl

    def get_ipv4fs_table(self):
        """Returns global IPv4 Flow Specification table.

        Creates the table if it does not exist.
        """
        ipv4fs_table = self._global_tables.get(RF_IPv4_FLOWSPEC)
        # Lazy initialization of the table.
        if not ipv4fs_table:
            ipv4fs_table = IPv4FlowSpecTable(self._core_service,
                                             self._signal_bus)
            self._global_tables[RF_IPv4_FLOWSPEC] = ipv4fs_table
            self._tables[(None, RF_IPv4_FLOWSPEC)] = ipv4fs_table

        return ipv4fs_table

    def get_ipv6fs_table(self):
        """Returns global IPv6 Flow Specification table.

        Creates the table if it does not exist.
        """
        ipv6fs_table = self._global_tables.get(RF_IPv6_FLOWSPEC)
        # Lazy initialization of the table.
        if not ipv6fs_table:
            ipv6fs_table = IPv6FlowSpecTable(self._core_service,
                                             self._signal_bus)
            self._global_tables[RF_IPv6_FLOWSPEC] = ipv6fs_table
            self._tables[(None, RF_IPv6_FLOWSPEC)] = ipv6fs_table

        return ipv6fs_table

    def get_vpnv4fs_table(self):
        """Returns global VPNv4 Flow Specification table.

        Creates the table if it does not exist.
        """
        vpnv4fs_table = self._global_tables.get(RF_VPNv4_FLOWSPEC)
        # Lazy initialization of the table.
        if not vpnv4fs_table:
            vpnv4fs_table = VPNv4FlowSpecTable(self._core_service,
                                               self._signal_bus)
            self._global_tables[RF_VPNv4_FLOWSPEC] = vpnv4fs_table
            self._tables[(None, RF_VPNv4_FLOWSPEC)] = vpnv4fs_table

        return vpnv4fs_table

    def get_vpnv6fs_table(self):
        """Returns global VPNv6 Flow Specification table.

        Creates the table if it does not exist.
        """
        vpnv6fs_table = self._global_tables.get(RF_VPNv6_FLOWSPEC)
        # Lazy initialization of the table.
        if not vpnv6fs_table:
            vpnv6fs_table = VPNv6FlowSpecTable(self._core_service,
                                               self._signal_bus)
            self._global_tables[RF_VPNv6_FLOWSPEC] = vpnv6fs_table
            self._tables[(None, RF_VPNv6_FLOWSPEC)] = vpnv6fs_table

        return vpnv6fs_table

    def get_l2vpnfs_table(self):
        """Returns global L2VPN Flow Specification table.

        Creates the table if it does not exist.
        """
        l2vpnfs_table = self._global_tables.get(RF_L2VPN_FLOWSPEC)
        # Lazy initialization of the table.
        if not l2vpnfs_table:
            l2vpnfs_table = L2VPNFlowSpecTable(self._core_service,
                                               self._signal_bus)
            self._global_tables[RF_L2VPN_FLOWSPEC] = l2vpnfs_table
            self._tables[(None, RF_L2VPN_FLOWSPEC)] = l2vpnfs_table

        return l2vpnfs_table

    def get_nexthop_label(self, label_key):
        return self._next_hop_label.get(label_key, None)

    def set_nexthop_label(self, key, value):
        self._next_hop_label[key] = value

    def update_vrf_table_links(self, vrf_table, new_imp_rts,
                               removed_imp_rts):
        """Update mapping from RT to VRF table."""
        assert vrf_table
        if new_imp_rts:
            self._link_vrf_table(vrf_table, new_imp_rts)
        if removed_imp_rts:
            self._remove_links_to_vrf_table_for_rts(vrf_table,
                                                    removed_imp_rts)

    def re_install_net_ctrl_paths(self, vrf_table):
        """Re-installs paths from NC with current BGP policy.

        Iterates over known paths from NC installed in `vrf4_table` and
        adds new path with path attributes as per current VRF configuration.
        """
        assert vrf_table
        for dest in vrf_table.values():
            for path in dest.known_path_list:
                if path.source is None:
                    vrf_table.insert_vrf_path(
                        nlri=path.nlri,
                        next_hop=path.nexthop,
                        gen_lbl=True
                    )
        LOG.debug('Re-installed NC paths with current policy for table %s.',
                  vrf_table)

    def _remove_links_to_vrf_table(self, vrf_table):
        """Removes any links to given `vrf_table`."""
        assert vrf_table
        vrf_conf = vrf_table.vrf_conf
        self._remove_links_to_vrf_table_for_rts(vrf_table,
                                                vrf_conf.import_rts)

    def _remove_links_to_vrf_table_for_rts(self, vrf_table, rts):
        rts_with_no_table = set()
        affected_tables = set()
        route_family = vrf_table.route_family
        for rt in rts:
            rt_rf_id = rt + ':' + str(route_family)
            rt_specific_tables = self._tables_for_rt.get(rt_rf_id)
            affected_tables.update(rt_specific_tables)
            if rt_specific_tables:
                try:
                    rt_specific_tables.remove(vrf_table)
                except KeyError:
                    LOG.debug('Did not find table listed as interested '
                              'for its import RT: %s', rt)
                if len(rt_specific_tables) == 0:
                    rts_with_no_table.add(rt)

        # Remove records of RT that have no tables associated with it.
        for rt in rts_with_no_table:
            rt_rf_id = rt + ':' + str(route_family)
            del self._tables_for_rt[rt_rf_id]

    def create_and_link_vrf_table(self, vrf_conf):
        """Factory method to create VRF table for given `vrf_conf`.

        Adds mapping to this table with appropriate scope. Also, adds mapping
        for import RT of this VRF to created table to facilitate
        importing/installing of paths from global tables.
        Returns created table.
        """
        route_family = vrf_conf.route_family

        if route_family == VRF_RF_IPV4:
            vrf_table = Vrf4Table
        elif route_family == VRF_RF_IPV6:
            vrf_table = Vrf6Table
        elif route_family == VRF_RF_L2_EVPN:
            vrf_table = VrfEvpnTable
        elif route_family == VRF_RF_IPV4_FLOWSPEC:
            vrf_table = Vrf4FlowSpecTable
        elif route_family == VRF_RF_IPV6_FLOWSPEC:
            vrf_table = Vrf6FlowSpecTable
        elif route_family == VRF_RF_L2VPN_FLOWSPEC:
            vrf_table = L2vpnFlowSpecTable
        else:
            raise ValueError('Unsupported route family for VRF: %s' %
                             route_family)

        vrf_table = vrf_table(vrf_conf, self._core_service, self._signal_bus)
        table_id = (vrf_conf.route_dist, route_family)
        self._tables[table_id] = vrf_table

        assert vrf_table is not None
        LOG.debug('Added new VrfTable with route_dist:%s and route_family:%s',
                  vrf_conf.route_dist, route_family)

        import_rts = vrf_conf.import_rts
        # If VRF is configured with import RT, we put this table
        # in a list corresponding to this RT for easy access.
        if import_rts:
            self._link_vrf_table(vrf_table, import_rts)

        return vrf_table

    def _link_vrf_table(self, vrf_table, rt_list):
        route_family = vrf_table.route_family
        for rt in rt_list:
            rt_rf_id = rt + ':' + str(route_family)
            table_set = self._tables_for_rt.get(rt_rf_id)
            if table_set is None:
                table_set = set()
                self._tables_for_rt[rt_rf_id] = table_set
            table_set.add(vrf_table)
            LOG.debug('Added VrfTable %s to import RT table list: %s',
                      vrf_table, rt)

    def _clean_global_uninteresting_paths(self):
        """Marks paths that do not have any route targets of interest
        for withdrawal.

        Since global tables can have paths with route targets that are not
        interesting any more, we have to clean these paths so that appropriate
        withdraw are sent out to NC and other peers. Interesting route targets
        change as VRF are modified or some filter is that specify what route
        targets are allowed are updated. This clean up should only be done when
        a route target is no longer considered interesting and some paths with
        that route target was installing in any of the global table.
        """
        uninteresting_dest_count = 0
        interested_rts = self._rt_mgr.global_interested_rts
        LOG.debug('Cleaning uninteresting paths. Global interested RTs %s',
                  interested_rts)
        for route_family in [RF_IPv4_VPN, RF_IPv6_VPN, RF_RTC_UC]:
            # TODO(PH): We currently do not install RT_NLRI paths based on
            # extended path attributes (RT)
            if route_family == RF_RTC_UC:
                continue
            table = self.get_global_table_by_route_family(route_family)
            uninteresting_dest_count += \
                table.clean_uninteresting_paths(interested_rts)

        LOG.debug('Found %s number of destinations had uninteresting paths.',
                  uninteresting_dest_count)

    def import_single_vpn_path_to_all_vrfs(self, vpn_path, path_rts=None):
        """Imports *vpn_path* to qualifying VRF tables.

        Import RTs of VRF table is matched with RTs from *vpn4_path* and if we
        have any common RTs we import the path into VRF.
        """
        LOG.debug('Importing path %s to qualifying VRFs', vpn_path)

        # If this path has no RTs we are done.
        if not path_rts:
            LOG.info('Encountered a path with no RTs: %s', vpn_path)
            return

        # We match path RTs with all VRFs that are interested in them.
        interested_tables = set()

        # Get route family of VRF to when this VPN Path can be imported to
        if vpn_path.route_family == RF_IPv4_VPN:
            route_family = RF_IPv4_UC
        elif vpn_path.route_family == RF_IPv6_VPN:
            route_family = RF_IPv6_UC
        elif vpn_path.route_family == RF_L2_EVPN:
            route_family = RF_L2_EVPN
        elif vpn_path.route_family == RF_VPNv4_FLOWSPEC:
            route_family = RF_IPv4_FLOWSPEC
        elif vpn_path.route_family == RF_VPNv6_FLOWSPEC:
            route_family = RF_IPv6_FLOWSPEC
        elif vpn_path.route_family == RF_L2VPN_FLOWSPEC:
            route_family = RF_L2VPN_FLOWSPEC
        else:
            raise ValueError('Unsupported route family for VRF: %s' %
                             vpn_path.route_family)

        for rt in path_rts:
            rt_rf_id = rt + ':' + str(route_family)
            vrf_rt_tables = self._tables_for_rt.get(rt_rf_id)
            if vrf_rt_tables:
                interested_tables.update(vrf_rt_tables)

        if interested_tables:
            # We iterate over all VRF tables that are interested in the RT
            # of the given path and import this path into them.
            route_dist = vpn_path.nlri.route_dist
            for vrf_table in interested_tables:
                if (vpn_path.source is not None or
                        route_dist != vrf_table.vrf_conf.route_dist):
                    update_vrf_dest = vrf_table.import_vpn_path(vpn_path)
                    # Queue the destination for further processing.
                    if update_vrf_dest is not None:
                        self._signal_bus.\
                            dest_changed(update_vrf_dest)
        else:
            # If we do not have any VRF with import RT that match with path RT
            LOG.debug('No VRF table found that imports RTs: %s', path_rts)

    def update_vrf_table(self, route_dist, prefix=None, next_hop=None,
                         route_family=None, route_type=None, tunnel_type=None,
                         is_withdraw=False, redundancy_mode=None,
                         pmsi_tunnel_type=None, **kwargs):
        """Update a BGP route in the VRF table identified by `route_dist`
        with the given `next_hop`.

        If `is_withdraw` is False, which is the default, add a BGP route
        to the VRF table identified by `route_dist` with the given
        `next_hop`.
        If `is_withdraw` is True, remove a BGP route from the VRF table
        and the given `next_hop` is ignored.

        If `route_family` is VRF_RF_L2_EVPN, `route_type` and `kwargs`
        are required to construct EVPN NLRI and `prefix` is ignored.

        ``redundancy_mode`` specifies a redundancy mode type.

`       `pmsi_tunnel_type` specifies the type of the PMSI tunnel attribute
         used to encode the multicast tunnel identifier.
        This field is advertised only if route_type is
        EVPN_MULTICAST_ETAG_ROUTE.

        Returns assigned VPN label.
        """
        from ryu.services.protocols.bgp.core import BgpCoreError

        assert route_dist

        if is_withdraw:
            gen_lbl = False
            next_hop = None
        else:
            gen_lbl = True
            if not (is_valid_ipv4(next_hop) or is_valid_ipv6(next_hop)):
                raise BgpCoreError(
                    desc='Invalid IPv4/IPv6 nexthop: %s' % next_hop)

        vrf_table = self._tables.get((route_dist, route_family))
        if vrf_table is None:
            raise BgpCoreError(
                desc='VRF table  does not exist: route_dist=%s, '
                     'route_family=%s' % (route_dist, route_family))

        vni = kwargs.get('vni', None)

        if route_family == VRF_RF_IPV4:
            if not is_valid_ipv4_prefix(prefix):
                raise BgpCoreError(desc='Invalid IPv4 prefix: %s' % prefix)
            ip, masklen = prefix.split('/')
            prefix = IPAddrPrefix(int(masklen), ip)
        elif route_family == VRF_RF_IPV6:
            if not is_valid_ipv6_prefix(prefix):
                raise BgpCoreError(desc='Invalid IPv6 prefix: %s' % prefix)
            ip6, masklen = prefix.split('/')
            prefix = IP6AddrPrefix(int(masklen), ip6)
        elif route_family == VRF_RF_L2_EVPN:
            assert route_type
            if route_type == EvpnMacIPAdvertisementNLRI.ROUTE_TYPE_NAME:
                # MPLS labels will be assigned automatically
                kwargs['mpls_labels'] = []
            if route_type == EvpnInclusiveMulticastEthernetTagNLRI.ROUTE_TYPE_NAME:
                # Inclusive Multicast Ethernet Tag Route does not have "vni",
                # omit "vni" from "kwargs" here.
                vni = kwargs.pop('vni', None)
            subclass = EvpnNLRI._lookup_type_name(route_type)
            kwargs['route_dist'] = route_dist
            esi = kwargs.get('esi', None)
            if esi is not None:
                if isinstance(esi, dict):
                    esi_type = esi.get('type', 0)
                    esi_class = EvpnEsi._lookup_type(esi_type)
                    kwargs['esi'] = esi_class.from_jsondict(esi)
                else:  # isinstance(esi, numbers.Integral)
                    kwargs['esi'] = EvpnArbitraryEsi(
                        type_desc.Int9.from_user(esi))
            if vni is not None:
                # Disable to generate MPLS labels,
                # because encapsulation type is not MPLS.
                from ryu.services.protocols.bgp.api.prefix import (
                    TUNNEL_TYPE_VXLAN, TUNNEL_TYPE_NVGRE)
                assert tunnel_type in [
                    None, TUNNEL_TYPE_VXLAN, TUNNEL_TYPE_NVGRE]
                gen_lbl = False
            prefix = subclass(**kwargs)
        else:
            raise BgpCoreError(
                desc='Unsupported route family %s' % route_family)

        # We do not check if we have a path to given prefix, we issue
        # withdrawal. Hence multiple withdrawals have not side effect.
        return vrf_table.insert_vrf_path(
            nlri=prefix, next_hop=next_hop, gen_lbl=gen_lbl,
            is_withdraw=is_withdraw, redundancy_mode=redundancy_mode,
            vni=vni, tunnel_type=tunnel_type,
            pmsi_tunnel_type=pmsi_tunnel_type)

    def update_flowspec_vrf_table(self, flowspec_family, route_dist, rules,
                                  actions=None, is_withdraw=False):
        """Update a BGP route in the VRF table for Flow Specification.

        ``flowspec_family`` specifies one of the flowspec family name.

        ``route_dist`` specifies a route distinguisher value.

        ``rules`` specifies NLRIs of Flow Specification as
        a dictionary type value.

        `` actions`` specifies Traffic Filtering Actions of
        Flow Specification as a dictionary type value.

        If `is_withdraw` is False, which is the default, add a BGP route
        to the VRF table identified by `route_dist`.
        If `is_withdraw` is True, remove a BGP route from the VRF table.
        """
        from ryu.services.protocols.bgp.core import BgpCoreError
        from ryu.services.protocols.bgp.api.prefix import (
            FLOWSPEC_FAMILY_VPNV4,
            FLOWSPEC_FAMILY_VPNV6,
            FLOWSPEC_FAMILY_L2VPN,
        )

        if flowspec_family == FLOWSPEC_FAMILY_VPNV4:
            vrf_table = self._tables.get((route_dist, VRF_RF_IPV4_FLOWSPEC))
            prefix = FlowSpecIPv4NLRI.from_user(**rules)
            try:
                communities = create_v4flowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))
        elif flowspec_family == FLOWSPEC_FAMILY_VPNV6:
            vrf_table = self._tables.get((route_dist, VRF_RF_IPV6_FLOWSPEC))
            prefix = FlowSpecIPv6NLRI.from_user(**rules)
            try:
                communities = create_v6flowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))
        elif flowspec_family == FLOWSPEC_FAMILY_L2VPN:
            vrf_table = self._tables.get((route_dist, VRF_RF_L2VPN_FLOWSPEC))
            prefix = FlowSpecL2VPNNLRI.from_user(route_dist, **rules)
            try:
                communities = create_l2vpnflowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))
        else:
            raise BgpCoreError(
                desc='Unsupported flowspec_family %s' % flowspec_family)

        if vrf_table is None:
            raise BgpCoreError(
                desc='VRF table does not exist: route_dist=%s, '
                     'flowspec_family=%s' % (route_dist, flowspec_family))

        # We do not check if we have a path to given prefix, we issue
        # withdrawal. Hence multiple withdrawals have not side effect.
        vrf_table.insert_vrffs_path(
            nlri=prefix, communities=communities,
            is_withdraw=is_withdraw)

    def update_global_table(self, prefix, next_hop=None, is_withdraw=False):
        """Update a BGP route in the Global table for the given `prefix`
        with the given `next_hop`.

        If `is_withdraw` is False, which is the default, add a BGP route
        to the Global table.
        If `is_withdraw` is True, remove a BGP route from the Global table.
        """
        src_ver_num = 1
        peer = None
        # set mandatory path attributes
        origin = BGPPathAttributeOrigin(BGP_ATTR_ORIGIN_IGP)
        aspath = BGPPathAttributeAsPath([[]])

        pathattrs = OrderedDict()
        pathattrs[BGP_ATTR_TYPE_ORIGIN] = origin
        pathattrs[BGP_ATTR_TYPE_AS_PATH] = aspath

        net = netaddr.IPNetwork(prefix)
        addr = str(net.ip)
        masklen = net.prefixlen
        if ip.valid_ipv4(addr):
            _nlri = IPAddrPrefix(masklen, addr)
            if next_hop is None:
                next_hop = '0.0.0.0'
            p = Ipv4Path
        else:
            _nlri = IP6AddrPrefix(masklen, addr)
            if next_hop is None:
                next_hop = '::'
            p = Ipv6Path

        new_path = p(peer, _nlri, src_ver_num,
                     pattrs=pathattrs, nexthop=next_hop,
                     is_withdraw=is_withdraw)

        # add to global table and propagates to neighbors
        self.learn_path(new_path)

    def update_flowspec_global_table(self, flowspec_family, rules,
                                     actions=None, is_withdraw=False):
        """Update a BGP route in the Global table for Flow Specification.

        ``flowspec_family`` specifies one of the Flow Specification
         family name.

        ``rules`` specifies NLRIs of Flow Specification as
        a dictionary type value.

        `` actions`` specifies Traffic Filtering Actions of
        Flow Specification as a dictionary type value.

        If `is_withdraw` is False, which is the default, add a BGP route
        to the Global table.
        If `is_withdraw` is True, remove a BGP route from the Global table.
        """

        from ryu.services.protocols.bgp.core import BgpCoreError
        from ryu.services.protocols.bgp.api.prefix import (
            FLOWSPEC_FAMILY_IPV4,
            FLOWSPEC_FAMILY_IPV6,
            FLOWSPEC_FAMILY_L2VPN,
        )

        src_ver_num = 1
        peer = None

        # set mandatory path attributes
        origin = BGPPathAttributeOrigin(BGP_ATTR_ORIGIN_IGP)
        aspath = BGPPathAttributeAsPath([[]])

        pathattrs = OrderedDict()
        pathattrs[BGP_ATTR_TYPE_ORIGIN] = origin
        pathattrs[BGP_ATTR_TYPE_AS_PATH] = aspath

        if flowspec_family == FLOWSPEC_FAMILY_IPV4:
            _nlri = FlowSpecIPv4NLRI.from_user(**rules)
            p = IPv4FlowSpecPath

            try:
                communities = create_v4flowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))

            if communities:
                pathattrs[BGP_ATTR_TYPE_EXTENDED_COMMUNITIES] = (
                    BGPPathAttributeExtendedCommunities(
                        communities=communities))
        elif flowspec_family == FLOWSPEC_FAMILY_IPV6:
            _nlri = FlowSpecIPv6NLRI.from_user(**rules)
            p = IPv6FlowSpecPath

            try:
                communities = create_v6flowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))

            if communities:
                pathattrs[BGP_ATTR_TYPE_EXTENDED_COMMUNITIES] = (
                    BGPPathAttributeExtendedCommunities(
                        communities=communities))
        elif flowspec_family == FLOWSPEC_FAMILY_L2VPN:
            _nlri = FlowSpecL2VPNNLRI.from_user(**rules)
            p = L2vpnFlowSpecPath

            try:
                communities = create_l2vpnflowspec_actions(actions)
            except ValueError as e:
                raise BgpCoreError(desc=str(e))

            if communities:
                pathattrs[BGP_ATTR_TYPE_EXTENDED_COMMUNITIES] = (
                    BGPPathAttributeExtendedCommunities(
                        communities=communities))
        else:
            raise BgpCoreError(
                desc='Unsupported flowspec family %s' % flowspec_family)

        new_path = p(peer, _nlri, src_ver_num,
                     pattrs=pathattrs, is_withdraw=is_withdraw)

        # add to global table and propagates to neighbors
        self.learn_path(new_path)

    def clean_stale_routes(self, peer, route_family=None):
        """Removes old routes from `peer` from `route_family` table.

        Routes/paths version number is compared with `peer`s current version
        number.
        """

        if route_family is not None:
            if route_family not in SUPPORTED_GLOBAL_RF:
                raise ValueError('Given route family %s is not supported.' %
                                 route_family)

            tables = [self._global_tables.get(route_family)]
        else:
            tables = self._global_tables.values()
        for table in tables:
            table.cleanup_paths_for_peer(peer)
