"""Manage flooding/learning on stacked datapaths."""

# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import copy

from faucet import valve_of
from faucet.valve_switch_standalone import ValveSwitchManager
from faucet.vlan import NullVLAN


class ValveSwitchStackManagerBase(ValveSwitchManager):
    """Base class for dataplane based flooding/learning on stacked dataplanes."""

    # By default, no reflection used for flooding algorithms.
    _USES_REFLECTION = False

    def __init__(self, stack_manager, **kwargs):
        super().__init__(**kwargs)

        self.stack_manager = stack_manager

        self._set_ext_port_flag = ()
        self._set_nonext_port_flag = ()
        self.external_root_only = False
        if self.has_externals:
            self.logger.info('external ports present, using loop protection')
            self._set_ext_port_flag = (self.flood_table.set_external_forwarding_requested(),)
            self._set_nonext_port_flag = (self.flood_table.set_no_external_forwarding_requested(),)
            if (not self.stack_manager.stack.is_root()
                    and self.stack_manager.stack.is_root_candidate()):
                self.logger.info('external flooding on root only')
                self.external_root_only = True

    @staticmethod
    def _non_stack_learned(other_valves, pkt_meta):
        """
        Obtain DP that has learnt the host that sent the packet

        Args:
            other_valves (list): Other valves
            pkt_meta (PacketMeta): Packet meta sent by the host
        Returns:
            DP: DP that has learnt the host
        """
        other_local_dp_entries = []
        other_external_dp_entries = []
        vlan_vid = pkt_meta.vlan.vid
        for other_valve in other_valves:
            other_dp_vlan = other_valve.dp.vlans.get(vlan_vid, None)
            if other_dp_vlan is not None:
                entry = other_dp_vlan.cached_host(pkt_meta.eth_src)
                if not entry:
                    continue
                if not entry.port.non_stack_forwarding():
                    continue
                if entry.port.loop_protect_external:
                    other_external_dp_entries.append(other_valve.dp)
                else:
                    other_local_dp_entries.append(other_valve.dp)
        # Another DP has learned locally, has priority.
        if other_local_dp_entries:
            return other_local_dp_entries[0]
        # No other DP has learned locally, but at least one has learned externally.
        if other_external_dp_entries:
            entry = pkt_meta.vlan.cached_host(pkt_meta.eth_src)
            # This DP has not learned the host either, use other's external.
            if entry is None:
                return other_external_dp_entries[0]
        return None

    def _external_forwarding_requested(self, port):
        external_forwarding_requested = None
        if self.has_externals:
            if port.tagged_vlans and port.loop_protect_external:
                external_forwarding_requested = False
            elif not port.stack:
                external_forwarding_requested = True
        return external_forwarding_requested

    def _build_flood_acts_for_port(self, vlan, exclude_unicast, port,  # pylint: disable=too-many-arguments
                                   exclude_all_external=False,
                                   exclude_restricted_bcast_arpnd=False):
        if self.external_root_only:
            exclude_all_external = True
        return super()._build_flood_acts_for_port(
            vlan, exclude_unicast, port,
            exclude_all_external=exclude_all_external,
            exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)

    def _flood_actions(self, in_port, external_ports,  # pylint: disable=too-many-arguments
                       away_flood_actions, toward_flood_actions, local_flood_actions):
        raise NotImplementedError

    def _build_flood_rule_actions(self, vlan, exclude_unicast, in_port,  # pylint: disable=too-many-arguments
                                  exclude_all_external=False, exclude_restricted_bcast_arpnd=False):
        """Compiles all the possible flood rule actions for a port on a stack node"""
        exclude_ports = list(self.stack_manager.inactive_away_ports)
        external_ports = vlan.loop_protect_external_ports()
        if in_port and self.stack_manager.is_stack_port(in_port):
            in_port_peer_dp = in_port.stack['dp']
            exclude_ports = exclude_ports + self.stack_manager.adjacent_stack_ports(in_port_peer_dp)
        local_flood_actions = tuple(self._build_flood_local_rule_actions(
            vlan, exclude_unicast, in_port, exclude_all_external, exclude_restricted_bcast_arpnd))
        away_flood_actions = tuple(valve_of.flood_tagged_port_outputs(
            self.stack_manager.away_ports, in_port, exclude_ports=exclude_ports))
        toward_flood_actions = tuple(valve_of.flood_tagged_port_outputs(
            self.stack_manager.chosen_towards_ports, in_port))
        flood_acts = self._flood_actions(
            in_port, external_ports, away_flood_actions,
            toward_flood_actions, local_flood_actions)
        return flood_acts

    def _build_mask_flood_rules_filters(self, port, vlan, eth_dst, eth_dst_mask, prune):
        """Builds filter for the input table to filter packets on ports that are pruned"""
        ofmsgs = []

        match = {'in_port': port.number, 'vlan': vlan}
        if eth_dst is not None:
            match.update({'eth_dst': eth_dst, 'eth_dst_mask': eth_dst_mask})

        replace_priority_offset = (self.classification_offset - (
            self.pipeline.filter_priority - self.pipeline.select_priority))

        priority_offset = replace_priority_offset
        if eth_dst is None:
            priority_offset -= 1

        if prune:
            # Allow the prune rule to be replaced with OF strict matching if
            # this port is unpruned later.
            ofmsgs.extend(self.pipeline.filter_packets(
                match, priority_offset=priority_offset))
        else:
            ofmsgs.extend(self.pipeline.remove_filter(
                match, priority_offset=priority_offset))
            # Control learning from multicast/broadcast on non-root DPs.
            if (not self.stack_manager.stack.is_root()
                    and eth_dst is not None and self._USES_REFLECTION):
                # If this is an edge DP, we don't have to learn from
                # hosts that only broadcast.  If we're an intermediate
                # DP, only learn from broadcasts further away from
                # the root (and ignore the reflected broadcast for
                # learning purposes).
                if self.stack_manager.stack.is_edge() or self.stack_manager.is_towards_root(port):
                    ofmsgs.extend(self.pipeline.select_packets(
                        self.flood_table, match,
                        priority_offset=self.classification_offset))
        return ofmsgs

    def _build_mask_flood_rules_flood_acts(self, vlan, eth_type, eth_dst, eth_dst_mask,
                                           exclude_unicast, exclude_restricted_bcast_arpnd,
                                           command, cold_start, prune, port):  # pylint: disable=unused-argument
        """Builds the flood rules for the flood table to forward packets along the stack topology"""
        ofmsgs = []
        flood_acts = []
        if self.has_externals:
            # If external flag is set, flood to external ports, otherwise exclude them.
            for ext_port_flag, exclude_all_external in (
                    (valve_of.PCP_NONEXT_PORT_FLAG, True),
                    (valve_of.PCP_EXT_PORT_FLAG, False)):
                if not prune:
                    flood_acts, _, _ = self._build_flood_acts_for_port(
                        vlan, exclude_unicast, port,
                        exclude_all_external=exclude_all_external,
                        exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)
                port_flood_ofmsg = self._build_flood_rule_for_port(
                    vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts,
                    add_match={valve_of.EXTERNAL_FORWARDING_FIELD: ext_port_flag})
                ofmsgs.append(port_flood_ofmsg)
        else:
            if not prune:
                flood_acts, _, _ = self._build_flood_acts_for_port(
                    vlan, exclude_unicast, port,
                    exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)
            port_flood_ofmsg = self._build_flood_rule_for_port(
                vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts)
            ofmsgs.append(port_flood_ofmsg)
        return ofmsgs

    def _build_mask_flood_rules(self, vlan, eth_type, eth_dst, eth_dst_mask,  # pylint: disable=too-many-arguments
                                exclude_unicast, exclude_restricted_bcast_arpnd,
                                command, cold_start):
        """Builds that flood rules for each mask for each port in the stack.
        This takes into account the pruned and non-pruned ports and returns
            the appropriate flood rule actions"""
        # Stack ports aren't in VLANs, so need special rules to cause flooding from them.
        ofmsgs = super()._build_mask_flood_rules(
            vlan, eth_type, eth_dst, eth_dst_mask,
            exclude_unicast, exclude_restricted_bcast_arpnd,
            command, cold_start)

        for port in self.stack_manager.stack_ports():

            if eth_dst is not None:
                # Prune broadcast flooding where multiply connected to same DP
                prune = self.stack_manager.is_pruned_port(port)
            else:
                # Do not prune unicast, may be reply from directly connected DP.
                prune = False

            ofmsgs.extend(self._build_mask_flood_rules_filters(
                port, vlan, eth_dst, eth_dst_mask, prune))
            ofmsgs.extend(self._build_mask_flood_rules_flood_acts(
                vlan, eth_type, eth_dst, eth_dst_mask,
                exclude_unicast, exclude_restricted_bcast_arpnd,
                command, cold_start, prune, port))

        return ofmsgs

    def edge_learn_port(self, other_valves, pkt_meta):
        """
        Find a port towards the edge DP where the packet originated from

        Args:
            other_valves (list): All Valves other than this one.
            pkt_meta (PacketMeta): PacketMeta instance for packet received.
        Returns:
            port to learn host on, or None.
        """
        # Got a packet from another DP.
        if pkt_meta.port.stack:
            # Received packet from
            edge_dp = self._edge_dp_for_host(other_valves, pkt_meta)
            if edge_dp:
                return self.stack_manager.edge_learn_port_towards(pkt_meta, edge_dp)
            # Assuming no DP has learned this host.
            return None

        # Got a packet locally.
        # If learning on an external port, check another DP hasn't
        # already learned on a local/non-external port.
        if pkt_meta.port.loop_protect_external:
            edge_dp = self._non_stack_learned(other_valves, pkt_meta)
            if edge_dp:
                return self.stack_manager.edge_learn_port_towards(pkt_meta, edge_dp)
        # Locally learn.
        return super().edge_learn_port(
            other_valves, pkt_meta)

    def _edge_dp_for_host(self, other_valves, pkt_meta):
        """Simple distributed unicast learning.

        Args:
            other_valves (list): All Valves other than this one.
            pkt_meta (PacketMeta): PacketMeta instance for packet received.
        Returns:
            Valve instance or None (of edge datapath where packet received)
        """
        raise NotImplementedError

    def add_drop_spoofed_faucet_mac_rules(self, vlan):
        """Install rules to drop spoofed faucet mac"""
        # antispoof for FAUCET's MAC address
        # TODO: antispoof for controller IPs on this VLAN, too.
        ofmsgs = []
        if self.drop_spoofed_faucet_mac:
            for port in self.ports.values():
                if not port.stack:
                    ofmsgs.extend(self.pipeline.filter_packets(
                        {'eth_src': vlan.faucet_mac, 'in_port': port.number}))
        return ofmsgs

    def add_port(self, port):
        ofmsgs = super().add_port(port)
        # If this is a stacking port, accept all VLANs (came from another FAUCET)
        if port.stack:
            # Actual stack traffic will have VLAN tags.
            ofmsgs.append(self.vlan_table.flowdrop(
                match=self.vlan_table.match(
                    in_port=port.number,
                    vlan=NullVLAN()),
                priority=self.low_priority + 1))
            ofmsgs.append(self.vlan_table.flowmod(
                match=self.vlan_table.match(in_port=port.number),
                priority=self.low_priority,
                inst=self.pipeline.accept_to_classification()))
        return ofmsgs

    def del_port(self, port):
        ofmsgs = super().del_port(port)
        if port.stack:
            for vlan in self.vlans.values():
                vlan.clear_cache_hosts_on_port(port)
            ofmsgs.extend(self._del_host_flows(port))
        return ofmsgs

    def get_lacp_dpid_nomination(self, lacp_id, valve, other_valves):
        """Chooses the DP for a given LAG.

        The DP will be nominated by the following conditions in order:
            1) Number of LAG ports
            2) Root DP
            3) Lowest DPID

        Args:
            lacp_id: The LACP LAG ID
            other_valves (list): list of other valves
        Returns:
            nominated_dpid, reason
        """
        if not other_valves:
            return None, ''
        stacked_other_valves = self.stack_manager.stacked_valves(other_valves)
        all_stacked_valves = {valve}.union(stacked_other_valves)
        ports = {}
        no_sync_ports = {}
        root_dpid = None
        for stack_valve in all_stacked_valves:
            all_lags = stack_valve.dp.lags_up()
            if lacp_id in all_lags:
                ports[stack_valve.dp.dp_id] = len(all_lags[lacp_id])
            nosync_lags = stack_valve.dp.lags_nosync()
            for l_id in nosync_lags:
                ports.setdefault(stack_valve.dp.dp_id, 0)
                no_sync_ports[stack_valve.dp.dp_id] = len(nosync_lags.get(l_id, 0))
            if stack_valve.dp.stack.is_root():
                root_dpid = stack_valve.dp.dp_id
        # Order by number of ports
        port_order = sorted(ports,
                            key=lambda port: (ports.get(port, 0), no_sync_ports.get(port, 0)),
                            reverse=True)
        if not port_order:
            return None, ''
        most_ports_dpid = port_order[0]
        most_ports_dpids = [dpid for dpid, num in ports.items() if num == ports[most_ports_dpid]]
        if len(most_ports_dpids) > 1:
            # There are several dpids that have the same number of lags
            if root_dpid in most_ports_dpids:
                # root_dpid is the chosen DPID
                return root_dpid, 'root dp'
            # Order by lowest DPID
            return sorted(most_ports_dpids), 'lowest dpid'
        # Most_ports_dpid is the chosen DPID
        return most_ports_dpid, 'most LAG ports'

    def _learn_host_intervlan_routing_flows(self, port, vlan, eth_src, eth_dst):
        """Returns flows for the eth_src_table that enable packets that have been
           routed to be accepted from an adjacent DP and then switched to the destination.
           Eth_src_table flow rule to match on port, eth_src, eth_dst and vlan

        Args:
            port (Port): Port to match on.
            vlan (VLAN): VLAN to match on
            eth_src: source MAC address (should be the router MAC)
            eth_dst: destination MAC address
        """
        ofmsgs = []
        (src_rule_idle_timeout, src_rule_hard_timeout, _) = self._learn_host_timeouts(port, eth_src)
        src_match = self.eth_src_table.match(vlan=vlan, eth_src=eth_src, eth_dst=eth_dst)
        src_priority = self.host_priority - 1
        inst = (self.eth_src_table.goto(self.output_table),)
        ofmsgs.extend([self.eth_src_table.flowmod(
            match=src_match,
            priority=src_priority,
            inst=inst,
            hard_timeout=src_rule_hard_timeout,
            idle_timeout=src_rule_idle_timeout)])
        return ofmsgs

    def _valve_learn_host_from_pkt(self, valve, now, pkt_meta, other_valves):
        """Add L3 forwarding rule if necessary for inter-VLAN routing."""
        ofmsgs_by_valve = super().learn_host_from_pkt(
            valve, now, pkt_meta, other_valves)
        if self.stack_manager.stack.route_learning and not self.stack_manager.stack.is_root():
            if pkt_meta.eth_src == pkt_meta.vlan.faucet_mac:
                ofmsgs_by_valve[valve].extend(self._learn_host_intervlan_routing_flows(
                    pkt_meta.port, pkt_meta.vlan, pkt_meta.eth_src, pkt_meta.eth_dst))
            elif pkt_meta.eth_dst == pkt_meta.vlan.faucet_mac:
                ofmsgs_by_valve[valve].extend(self._learn_host_intervlan_routing_flows(
                    pkt_meta.port, pkt_meta.vlan, pkt_meta.eth_dst, pkt_meta.eth_src))
        return ofmsgs_by_valve

    def learn_host_from_pkt(self, valve, now, pkt_meta, other_valves):
        ofmsgs_by_valve = {}

        if self.stack_manager.stack.route_learning:
            stacked_other_valves = self.stack_manager.stacked_valves(other_valves)
            all_stacked_valves = {valve}.union(stacked_other_valves)

            # NOTE: multi DP routing requires learning from directly attached switch first.
            if pkt_meta.port.stack:
                peer_dp = pkt_meta.port.stack['dp']
                if peer_dp.dyn_running:
                    faucet_macs = {pkt_meta.vlan.faucet_mac}.union(
                        {valve.dp.faucet_dp_mac for valve in all_stacked_valves})
                    # Must always learn FAUCET VIP, but rely on neighbor
                    # to learn other hosts first.
                    if pkt_meta.eth_src not in faucet_macs:
                        return ofmsgs_by_valve

            for other_valve in stacked_other_valves:
                stack_port = other_valve.stack_manager.relative_port_towards(
                    self.stack_manager.stack.name)
                valve_vlan = other_valve.dp.vlans.get(pkt_meta.vlan.vid, None)
                if stack_port and valve_vlan:
                    valve_pkt_meta = copy.copy(pkt_meta)
                    valve_pkt_meta.vlan = valve_vlan
                    valve_pkt_meta.port = stack_port
                    valve_other_valves = all_stacked_valves - {other_valve}
                    ofmsgs_by_valve.update(self._valve_learn_host_from_pkt(
                        other_valve, now, valve_pkt_meta, valve_other_valves))

        ofmsgs_by_valve.update(
            self._valve_learn_host_from_pkt(valve, now, pkt_meta, other_valves))
        return ofmsgs_by_valve


class ValveSwitchStackManagerNoReflection(ValveSwitchStackManagerBase):
    """Stacks of size 2 - all switches directly connected to root.

    Root switch simply floods to all other switches.

    Non-root switches simply flood to the root.
    """

    def _flood_actions(self, in_port, external_ports,  # pylint: disable=too-many-arguments
                       away_flood_actions, toward_flood_actions, local_flood_actions):
        if not in_port or self.stack_manager.is_stack_port(in_port):
            flood_prefix = ()
        else:
            if external_ports:
                flood_prefix = self._set_nonext_port_flag
            else:
                flood_prefix = self._set_ext_port_flag

        flood_actions = (
            flood_prefix + toward_flood_actions + away_flood_actions + local_flood_actions)

        return flood_actions

    def _edge_dp_for_host(self, other_valves, pkt_meta):
        """Size 2 means root shortest path is always directly connected."""
        peer_dp = pkt_meta.port.stack['dp']
        if peer_dp.dyn_running:
            return self._non_stack_learned(other_valves, pkt_meta)
        # Fall back to assuming peer knows if we are not the peer's controller.
        return peer_dp


class ValveSwitchStackManagerReflection(ValveSwitchStackManagerBase):
    """Stacks size > 2 reflect floods off of root (selective flooding).

       .. code-block:: none

                                Hosts
                                 ||||
                                 ||||
                   +----+       +----+       +----+
                ---+1   |       |1234|       |   1+---
          Hosts ---+2   |       |    |       |   2+--- Hosts
                ---+3   |       |    |       |   3+---
                ---+4  5+-------+5  6+-------+5  4+---
                   +----+       +----+       +----+

                   Root DP

       Non-root switches flood only to the root. The root switch
       reflects incoming floods back out. Non-root switches flood
       packets from the root locally and to switches further away
       from the root. Flooding is entirely implemented in the dataplane.

       A host connected to a non-root switch can receive a copy of its
       own flooded packet (because the non-root switch does not know
       it has seen the packet already).

       A host connected to the root switch does not have this problem
       (because flooding is always away from the root). Therefore,
       connections to other non-FAUCET stacking networks should only
       be made to the root.

       On the root switch (left), flood destinations are:

       1: 2 3 4 5(s)
       2: 1 3 4 5(s)
       3: 1 2 4 5(s)
       4: 1 2 3 5(s)
       5: 1 2 3 4 5(s, note reflection)

       On the middle switch:

       1: 5(s)
       2: 5(s)
       3: 5(s)
       4: 5(s)
       5: 1 2 3 4 6(s)
       6: 5(s)

       On the rightmost switch:

       1: 5(s)
       2: 5(s)
       3: 5(s)
       4: 5(s)
       5: 1 2 3 4
    """

    # Indicate to base class use of reflection required.
    _USES_REFLECTION = True

    def _learn_cache_check(self, entry, vlan, now, eth_src, port, ofmsgs,
                           cache_port, cache_age,
                           delete_existing, refresh_rules):
        learn_exit = False
        update_cache = True
        if cache_port is not None:
            # packet was received on same member of a LAG.
            same_lag = (port.lacp and port.lacp == cache_port.lacp)
            # stacks of size > 2 will have an unknown MAC flooded towards the root,
            # and flooded down again. If we learned the MAC on a local port and
            # heard the reflected flooded copy, discard the reflection.
            local_stack_learn = port.stack and not cache_port.stack
            guard_time = self.cache_update_guard_time
            if cache_port == port or same_lag or local_stack_learn:
                port_cache_valid = (
                    port.dyn_update_time is not None and port.dyn_update_time <= entry.cache_time)
                # aggressively re-learn on LAGs, and prefer recently learned
                # locally learned hosts on a stack.
                if same_lag or local_stack_learn:
                    guard_time = 2
                # port didn't change status, and recent cache update, don't do anything.
                if cache_age < guard_time and port_cache_valid:
                    update_cache = False
                    learn_exit = True
                # skip delete if host didn't change ports or on same LAG.
                elif cache_port == port or same_lag:
                    delete_existing = False
                    if port_cache_valid:
                        refresh_rules = True
        return (learn_exit, ofmsgs, cache_port, update_cache, delete_existing, refresh_rules)

    def _flood_actions(self, in_port, external_ports,  # pylint: disable=too-many-arguments
                       away_flood_actions, toward_flood_actions, local_flood_actions):
        if self.stack_manager.stack.is_root():
            if external_ports:
                flood_prefix = self._set_nonext_port_flag
            else:
                flood_prefix = self._set_ext_port_flag
            flood_actions = (away_flood_actions + local_flood_actions)

            if in_port and self.stack_manager.is_away(in_port):
                # Packet from a non-root switch, flood locally and to all non-root switches
                # (reflect it).
                flood_actions = (
                    away_flood_actions + (valve_of.output_in_port(),) + local_flood_actions)

            flood_actions = flood_prefix + flood_actions
        else:
            # Default non-root strategy is flood towards root.
            if external_ports:
                flood_actions = self._set_nonext_port_flag + toward_flood_actions
            else:
                flood_actions = self._set_ext_port_flag + toward_flood_actions

            if in_port:
                # Packet from switch further away, flood it to the root.
                if self.stack_manager.is_away(in_port):
                    flood_actions = toward_flood_actions
                # Packet from the root.
                elif self.stack_manager.is_towards_root(in_port):
                    # If we have external ports, and packet hasn't already been flooded
                    # externally, flood it externally before passing it to further away switches,
                    # and mark it flooded.
                    if external_ports:
                        flood_actions = (
                            self._set_nonext_port_flag + away_flood_actions + local_flood_actions)
                    else:
                        flood_actions = (
                            away_flood_actions + self._set_nonext_port_flag + local_flood_actions)
                # Packet from external port, locally. Mark it already flooded externally and
                # flood to root (it came from an external switch so keep it within the stack).
                elif in_port.loop_protect_external:
                    flood_actions = self._set_nonext_port_flag + toward_flood_actions
                else:
                    flood_actions = self._set_ext_port_flag + toward_flood_actions

        return flood_actions

    def _edge_dp_for_host(self, other_valves, pkt_meta):
        """For stacks size > 2."""
        # TODO: currently requires controller to manage all switches
        # in the stack to keep each DP's graph consistent.
        # TODO: simplest possible unicast learning.
        # We find just one port that is the shortest unicast path to
        # the destination. We could use other factors (eg we could
        # load balance over multiple ports based on destination MAC).
        # Find port that forwards closer to destination DP that
        # has already learned this host (if any).
        peer_dp = pkt_meta.port.stack['dp']
        if peer_dp.dyn_running:
            return self._non_stack_learned(other_valves, pkt_meta)
        # Fall back to peer knows if edge or root if we are not the peer's controller.
        if peer_dp.stack.is_edge() or peer_dp.stack.is_root():
            return peer_dp
        # No DP has learned this host, yet. Take no action to allow remote learning to occur.
        return None
