<commit_before># Copyright (c) 2015 OpenStack Foundation.
# All Rights Reserved.
#
#    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 netaddr
import struct
import threading

from ryu.controller.handler import CONFIG_DISPATCHER
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.controller import ofp_event
from ryu.ofproto import ether
from ryu.ofproto import ofproto_v1_3

from ryu.lib.packet import arp
from ryu.lib.packet import ethernet
from ryu.lib.packet import packet

from ryu.lib.mac import haddr_to_bin
from ryu.lib.packet import icmp
from ryu.lib.packet import ipv4
from ryu.lib.packet import ipv6
from ryu.lib.packet import tcp
from ryu.lib.packet import udp

from ryu.lib import addrconv

from neutron import context

from neutron.common import constants as const
from neutron.i18n import _LE, _LI, _LW
from oslo_log import log

from dragonflow.controller.df_base_app import DFlowApp
from dragonflow.utils.bloomfilter import BloomFilter
LOG = log.getLogger(__name__)

ETHERNET = ethernet.ethernet.__name__
IPV4 = ipv4.ipv4.__name__
ICMP = icmp.icmp.__name__
TCP = tcp.tcp.__name__
UDP = udp.udp.__name__

VLANID_NONE = 0
VLANID_MIN = 2
VLANID_MAX = 4094
COOKIE_SHIFT_VLANID = 32
UINT16_MAX = 0xffff
UINT32_MAX = 0xffffffff
UINT64_MAX = 0xffffffffffffffff
OFPFW_NW_PROTO = 1 << 5

REG_32BIT_ON = 0x80000000

HIGH_PRIORITY_FLOW = 1000
MEDIUM_PRIORITY_FLOW = 100
NORMAL_PRIORITY_FLOW = 10
LOW_PRIORITY_FLOW = 1
LOWEST_PRIORITY_FLOW = 0


CONTROLLER_L3_CONFIGURED_FLOW_PRIORITY = 50
ROUTER_INTERFACE_FLOW_PRIORITY = 40
LOCAL_SUBNET_TRAFFIC_FLOW_PRIORITY = 30
EAST_WEST_TRAFFIC_TO_CONTROLLER_FLOW_PRIORITY = 20
SNAT_RULES_PRIORITY_FLOW = 10


class CookieFilter(BloomFilter):
    """Bloom Filter in a cookie

    Useful and delicious!

    To get a valid int cookie:
     cf = CookieFilter(keys)
     cookie = cf.to_cookie()

    When adding a flow:
     OFPModFlow(..., cookie=cookie)

    When matching a flow (cookie can also just be 0xFFFFFFFF):
     OFPModFlow(..., cookie=cookie, cookie_mask=cookie)
    """
    def __init__(self, keys=()):
        super(CookieFilter, self).__init__(
            num_bytes=8,
            num_probes=2,
            iterable=keys,
        )

    def to_cookie(self):
        return CookieFilter._array_to_int64(self.array)

    @staticmethod
    def from_route(route):
        """Create a filter for a route

        :type route: list of PortData objects
        :rtype: CookieFilter
        """

        # TODO(saggi) memoize
        return CookieFilter(port.cookie_hash for port in route)

    @staticmethod
    def from_port_data(port_data):
        """Create a CookieFilter from a single PortData object

        :type port_data: PortData
        :rtype: CookieFilter
        """
        # TODO(saggi) memoize
        return CookieFilter((port_data.cookie_hash,))

    @staticmethod
    def _array_to_int64(array):
        """Converts bloom filter mask or state to int64

        Only works if array length is 8

        :param array: Array of a bloom filter or mask
        :return: The resulting array encoded as a signed int
        :rtype: int
        """
        assert(len(array) == 8)
        return struct.unpack("Q", str(array))[0]


class AgentDatapath(object):
    """Represents a forwarding element switch local state"""

    def __init__(self):
        self.local_ports = None
        self.datapath = 0
        self.patch_port_num = 0

        # Dictionary used to hold port information received from OVS
        # each port data structure has a link to an entry in this dictionary
        # in 'switch_port_desc'
        self.switch_port_desc_dict = {}


class TenantTopology(object):
    """Represents a tenant topology"""

    def __init__(self, tenant_id):
        self.routers = {}
        self.mac_to_port_data = {}
        self.subnets = {}
        self.id = tenant_id

    def add_router(self, router):
        self.routers[router.id] = router

    def del_router(self, id):
        try:
            del self.routers[id]
        except KeyError:
            return -1

    def find_port_data_by_ip_address(self, ip_address):
        for port_data in self.mac_to_port_data.values():
            for fixed_ip in port_data.fixed_ips:
                if ip_address == fixed_ip['ip_address']:
                    return port_data, self.subnets[fixed_ip['subnet_id']]
        else:
            return 0, 0

    def find_port_data_by_local_name(self, local_port_name):
        """
        :param local_port_name: local name of the port on the switch
                                (eg. "qvo2dcdg-as")
        :type: local_port_name: str
        :return: A PortData object if one was found or None if no matching
                 object exists
        :type: PortData
        """

        name_prefix_len = 3
        partial_id = local_port_name[name_prefix_len:]
        for port_data in self.mac_to_port_data.values():
            if port_data.id.startswith(partial_id):
                return port_data
        else:
            return None

    def get_route(self, pkt):
        """Get a possible route the packet can take to reach it's destination

        The are no guarantees that the returned route is the fastest. Only that
        it's possible.

        Currently doesn't support extra routes.

        :param pkt: The packet to trace
        :return: A list of port_data objects if a route was found or None if no
                 route is available.
        """
        pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
        pkt_ethernet = pkt.get_protocol(ethernet.ethernet)

        in_port_data = self.mac_to_port_data.get(pkt_ethernet.src)
        out_port_data = self.mac_to_port_data.get(pkt_ethernet.dst)
        if in_port_data is None or out_port_data is None:
            LOG.error(
                _LE("No data for packet ports %(src_mac)s %(dst_mac)s"),
                {"src_mac": pkt_ethernet.src,
                 "dst_mac": pkt_ethernet.dst}
            )
            return None

        (dst_port_data, dst_subnet) = self.find_port_data_by_ip_address(
            pkt_ipv4.dst
        )
        if not dst_port_data:
            LOG.error(
                _LE("No data for destination port %(dst_ip)s"),
                {"dst_ip": pkt_ipv4.dst}
            )
            return None

        is_same_port = out_port_data.id == dst_port_data.id
        if is_same_port:
            return [in_port_data, out_port_data]
        # In order to hop the target has to be a router
        if not out_port_data.is_router_interface:
            LOG.error(
                _LE("The gateway port is not a router  %(dst_mac)s"),
                {"dst_mac": pkt_ethernet.dst}
            )
            return None

        gateway_router = self.routers.get(out_port_data.device_id)
        if gateway_router is None:
            return None

        if dst_subnet.id in gateway_router.subnets:
            # TODO(saggi) add second leg to route
            return [in_port_data, out_port_data, dst_port_data]

        # route not found
        return None

    @property
    def unused_subnets(self):
        unused_subnets = self.subnets.copy()
        for port in self.mac_to_port_data.values():
            for fixed_ip in port.fixed_ips:
                unused_subnets.pop(fixed_ip['subnet_id'], None)

            # Optimization, no need to keep filtering if it's empty
            if len(unused_subnets) == 0:
                break

        return unused_subnets.values()

    def get_routers_ports_by_subnet(self, subnet):
        ports = []
        try:
            for router in self.routers.values():
                for subnet_id in router.subnets:
                    for interface in router.interfaces:
                        for subnet_info in interface['subnets']:
                            if subnet.id == subnet_info['id']:
                                ports.append(interface)

        except KeyError:
            LOG.error(_LE("AttributeError: for %s"), subnet.id)

        return ports


class Router(object):

    def __init__(self, data):
        self.data = data
        self.subnets = []

    def add_subnet(self, subnet):
        self.subnets.append(subnet.id)

    def remove_subnet(self, subnet):
        self.subnets.remove(subnet.id)

    @property
    def id(self):
        return self.data['id']

    @property
    def interfaces(self):
        return self.data.get('_interfaces', ())


class Subnet(object):

    def __init__(self, data, segmentation_id):
        self.data = data
        self.segmentation_id = segmentation_id

    def set_data(self, data):
        self.data = data

    @property
    def id(self):
        try:
            return self.data['id']
        except TypeError:
            return -1

    @property
    def cidr(self):
        """Return the CIDR information for this subnet

        :return: CIDR information for subnet
        :rtype: netaddr.IPNetwork
        """
        return netaddr.IPNetwork(self.data['cidr'])

    @property
    def gateway_ip(self):
        return self.data.get('gateway_ip')

    def is_ipv4(self):
        try:
            return (netaddr.IPNetwork(self.cidr).ip).version == 4
        except TypeError:
            return False

    def __repr__(self):
        return ("<Subnet id='%s' cidr='%s' gateway_ip='%s' " +
                "segmentation_id='%s'>") % (
            self.id,
            self.cidr,
            self.gateway_ip,
            self.segmentation_id,
        )


class SnatBinding(object):

    def __init__(self, subnet, port):
        self.subnet_id = subnet
        self.sn_port = port
        self.segmentation_id = 0


class PortData(object):
    def __init__(self, port_data):
        self._port_data = port_data

    @property
    def is_router_interface(self):
        if self._port_data['device_owner'] in const.ROUTER_INTERFACE_OWNERS:
            return True
        else:
            return False

    @property
    def id(self):
        return self._port_data['id']

    @property
    def device_id(self):
        return self._port_data['device_id']

    @property
    def fixed_ips(self):
        try:
            return tuple(self._port_data.get('fixed_ips'))
        except KeyError:
            return tuple()

    @property
    def subnets(self):
        subnets = []
        for fixed_ip in self.fixed_ips:
            try:
                subnets.append(fixed_ip['subnet_id'])
            except KeyError:
                LOG.error(_LE("No subnet info for %s"), fixed_ip)
                continue
        return subnets

    @property
    def segmentation_id(self):
        return self._port_data['segmentation_id']

    @property
    def local_port_number(self):
        try:
            return self._port_data['switch_port_desc']['local_port_num']
        except KeyError:
            return -1

    @property
    def local_datapath_id(self):
        try:
            return self._port_data['switch_port_desc']['local_dpid_switch']
        except KeyError:
            return -1

    @property
    def mac_address(self):
        return self._port_data['mac_address']

    @property
    def cookie_hash(self):
        """Create an int64 to be used for ovs cookie needs.
        :return:
        :rtype: int
        """
        uuid_prefix = self.id.split('-', 1)[0]
        return int(uuid_prefix, 16)

    def get_subnet_from_ip_address(self, ip_address):
        for fixed_ip in self.fixed_ips:
            if ip_address == fixed_ip['ip_address']:
                return fixed_ip
        else:
            return None

    @property
    def is_gateway(self):
        for subnet in self._port_data['subnets']:
            for fixed_ip in self.fixed_ips:
                if subnet['id'] == fixed_ip['subnet_id']:
                    if subnet['gateway_ip'] == fixed_ip['ip_address']:
                        return True
        else:
            return False

    def __getitem__(self, item):
        return self._port_data.__getitem__(item)

    def __setitem__(self, key, value):
        return self._port_data.__setitem__(key, value)


class L3ReactiveApp(DFlowApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    BASE_RPC_API_VERSION = '1.0'

    BASE_TABLE = 0
    CLASSIFIER_TABLE = 40
    METADATA_TABLE_ID = 50
    ARP_AND_BR_TABLE = 51
    L3_VROUTER_TABLE = 52
    L3_PUBLIC_TABLE = 53
    TUN_TRANSLATE_TABLE = 60

    def __init__(self, *args, **kwargs):
        super(L3ReactiveApp, self).__init__(*args, **kwargs)
        self.mac_to_port = {}

        self.ctx = context.get_admin_context()
        self.lock = threading.Lock()
        self._tenants = {}
        self.need_sync = True
        self.dp_list = {}
        self.snat_bindings = {}
        self.idle_timeout = kwargs['idle_timeout']
        self.hard_timeout = kwargs['hard_timeout']

    def get_tenant_by_id(self, tenant_id):
        if tenant_id in self._tenants:
            return self._tenants[tenant_id]
        else:
            return self._tenants.setdefault(tenant_id,
                                            TenantTopology(tenant_id))

    def start(self):
        LOG.info(_LI("Starting Virtual L3 Reactive OpenFlow APP "))
        super(L3ReactiveApp, self).start()
        return 1

    def delete_router(self, router_id):
        for tenant in self._tenants.values():
            try:
                router = tenant.routers.pop(router_id)
            except KeyError:
                pass
            else:
                for interface in router.interfaces:
                    for subnet_info in interface['subnets']:
                        subnet = tenant.subnets[subnet_info['id']]
                        if subnet.segmentation_id == 0:
                            continue

                        if subnet.is_ipv4():
                            self._remove_vrouter_arp_responder_cast(
                                subnet.segmentation_id,
                                interface['mac_address'],
                                self.get_ip_from_interface(interface))

    def sync_router(self, router_info):
        LOG.info(_LI("sync_router --> %s"), router_info)
        tenant_topology = self.get_tenant_by_id(router_info['tenant_id'])

        router = Router(router_info)
        router_old = tenant_topology.routers.get(router.id)
        tenant_topology.add_router(router)
        subnets = tenant_topology.subnets

        for interface in router.interfaces:
            for subnet_info in interface['subnets']:
                subnet = subnets.setdefault(
                        subnet_info['id'],
                        Subnet(subnet_info, 0),
                )
                if subnet.data is None:
                    subnet.set_data(subnet_info)

                router.add_subnet(subnet)
                if subnet.segmentation_id != 0:
                    self.subnet_added_binding_cast(subnet, interface)
                    self.bootstrap_network_classifiers(
                        subnet=subnet)

        # If previous definition of the router is known
        if router_old:
            # Handle removed subnets
            for interface in router_old.interfaces:
                for subnet_info in interface['subnets']:
                    subnet = subnets[subnet_info['id']]
                    if subnet.segmentation_id == 0:
                        continue

                    # if subnet was not deleted
                    if subnet.id in router.subnets:
                        continue

                    if subnet.is_ipv4():
                        self._remove_vrouter_arp_responder_cast(
                            subnet.segmentation_id,
                            interface['mac_address'],
                            self.get_ip_from_interface(interface))
                        if PortData(interface).is_gateway:
                            self._handle_remove_subnet(subnet)

                        self._handle_remove_port(PortData(interface))

    def _handle_remove_subnet(self, subnet):
        """Remove all the flow relating to a specific subnet

        :param subnet:
        :type subnet: Subnet
        """
        for dp in self.dp_list.values():
            datapath = dp.datapath
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_ipv4_dst_masked(subnet.cidr.ip.value,
                                      subnet.cidr.netmask.value)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.L3_VROUTER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_metadata(subnet.segmentation_id)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.L3_VROUTER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)

    def attach_switch_port_desc_to_port_data(self, port_data):
        if 'id' in port_data:
            port_id = port_data['id']
            sub_str_port_id = str(port_id[0:11])

            # Only true if we already received port_desc from OVS
            for switch in self.dp_list.values():
                switch_port_desc_dict = switch.switch_port_desc_dict
                if sub_str_port_id in switch_port_desc_dict:
                    port_data['switch_port_desc'] = (
                        switch_port_desc_dict[sub_str_port_id])
                    port_desc = port_data['switch_port_desc']
                    self.add_flow_metadata_by_port_num(
                        port_desc['datapath'],
                        0,
                        HIGH_PRIORITY_FLOW,
                        port_desc['local_port_num'],
                        port_data['segmentation_id'],
                        0xffff,
                        self.CLASSIFIER_TABLE)

    def delete_port(self, port):
        """

        :param port:
        :type port: PortData
        """
        self._handle_remove_port(PortData(port))

    def _remove_flow_local_subnet(self, subnet):
        """

        :param subnet:
        :type subnet: Subnet
        """
        for dp in self.dp_list.values():
            datapath = dp.datapath
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_metadata(subnet.segmentation_id)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.CLASSIFIER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)

    def _find_tenant_for_port_data(self, port_data):
        """

        :param port_data:
        :type port_data: PortData
        :return:
        :rtype: TenantTopology or None
        """

        for tenant in self._tenants.values():
            if port_data.mac_address in tenant.mac_to_port_data:
                return tenant
        else:
            return None

    def sync_port(self, port):
        LOG.info(_LI("sync_port--> %s\n"), port)
        segmentation_id = port.get('segmentation_id')
        if segmentation_id is None:
            LOG.info(_LI("no segmentation data in port --> %s"), port)
            return

        tenant_topo = self.get_tenant_by_id(port['tenant_id'])
        for subnet_dict in port.get('subnets', []):
            subnet = tenant_topo.subnets.get(subnet_dict['id'])
            if subnet:
                subnet.set_data(subnet_dict)
                subnet.segmentation_id = segmentation_id
            else:
                tenant_topo.subnets[subnet_dict['id']] = subnet = Subnet(
                    subnet_dict,
                    segmentation_id)
            if port['device_owner'] == const.DEVICE_OWNER_ROUTER_INTF:
                self.subnet_added_binding_cast(subnet, port)
                self.bootstrap_network_classifiers(subnet=subnet)
            self._add_flow_normal_local_subnet_cast(
                    LOCAL_SUBNET_TRAFFIC_FLOW_PRIORITY,
                    subnet)
        tenant_topo.mac_to_port_data[port['mac_address']] = PortData(port)
        self.attach_switch_port_desc_to_port_data(port)

    def _get_input_packet_handler(self, pkt):
        is_ipv4_packet = pkt.get_protocol(ipv4.ipv4) is not None
        is_ipv6_packet = pkt.get_protocol(ipv6.ipv6) is not None

        packet_handler = None
        if is_ipv4_packet:
            packet_handler = self.handle_ipv4_packet_in

        elif is_ipv6_packet:
            packet_handler = self.handle_ipv6_packet_in

        return packet_handler

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def OF_packet_in_handler(self, event):
        msg = event.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto

        if msg.reason == ofproto.OFPR_NO_MATCH:
            reason = 'NO MATCH'
        elif msg.reason == ofproto.OFPR_ACTION:
            reason = 'ACTION'
        elif msg.reason == ofproto.OFPR_INVALID_TTL:
            reason = 'INVALID TTL'
        else:
            reason = 'unknown'

        LOG.debug('OFPPacketIn received: '
                  'buffer_id=%x total_len=%d reason=%s '
                  'table_id=%d cookie=%d match=%s',
                  msg.buffer_id, msg.total_len, reason,
                  msg.table_id, msg.cookie, msg.match)

        pkt = packet.Packet(msg.data)
        packet_handler = self._get_input_packet_handler(pkt)
        if packet_handler is None:
            LOG.error(_LE("Unable to find appropriate packet "
                          "handler for packet: %s"), pkt)
        else:
            try:
                packet_handler(datapath, msg, pkt)
            except Exception as exception:
                LOG.debug(
                    "Unable to handle packet %(msg)s: %(e)s",
                    {'msg': msg, 'e': exception}
                )

    def handle_ipv6_packet_in(self, datapath, msg, pkt):
        # TODO(gampel)(gampel) add ipv6 support
        LOG.error(_LE("No handle for ipv6 yet should be offload to the"
                "NORMAL path  %s"), pkt)
        return

    def is_known_datapath(self, datapath):
        """Check if datapath is known to this openflow appliaction"""
        return self.dp_list.get(datapath.id) is not None

    def _handle_router_packet(self, datapath, pkt, route):
        """Handle packets intended for routers

        Specifically OAM (Operations, administration and management) packets
        Does nothing if pkt is not a supported protocol.

        The function assumes the route is valid and the packet is meant for the
        last port in the route.

        Currently only handles ping

        :param datapath: The datapath to send through
        :param pkt: The packet to handle
        :param route: The resolved route the packet is it take
        """
        is_icmp_packet = pkt.get_protocol(icmp.icmp) is not None

        if is_icmp_packet:
            # send ping response
            self._handle_icmp(
                datapath,
                pkt,
                route[0],
            )

        else:
            pkt_ipv4 = pkt.get_protocol(ipv4.ipv4) is not None
            LOG.error(_LE("any communication to a router that "
                          "is not ping should be dropped from "
                          "ip '%s'"),
                      pkt_ipv4.src)

    def _handle_vm_packet(self, datapath, msg, pkt, route):
        """Handle packets intended for VMs
        """
        pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
        pkt_ethernet = pkt.get_protocol(ethernet.ethernet)

        LOG.debug(
            "Installing flow Route %s-> %s",
            pkt_ipv4.src,
            pkt_ipv4.dst)

        self.install_l3_forwarding_flows(
            datapath,
            msg,
            route[0],
            pkt_ethernet,
            pkt_ipv4,
            route[1],
            route[-1],
            CookieFilter.from_route(route),
        )

    def _get_tenant_for_msg(self, msg, pkt):
        segmentation_id = msg.match.get('metadata')
        if segmentation_id is None:
            return None

        pkt_eth = pkt.get_protocol(ethernet.ethernet)
        for tenant in self._tenants.values():
            port_data = tenant.mac_to_port_data.get(pkt_eth)
            if port_data is None:
                # target is router
                for subnet in tenant.subnets.values():
                    if subnet.segmentation_id == segmentation_id:
                        return tenant
            else:
                for fixed_ip in port_data.fixed_ips:
                    subnet = tenant.subnets.get(fixed_ip['subnet_id'])
                    if subnet is None:
                        continue

                    if subnet.segmentation_id == segmentation_id:
                        return tenant

    def handle_ipv4_packet_in(self, datapath, msg, pkt):
        if not self.is_known_datapath(datapath):
            LOG.warning(_LW("Received packet from unknown datapath '%s'"),
                        datapath.id)
            return

        segmentation_id = msg.match.get('metadata')
        if segmentation_id is None:
            # send request for local switch data
            self.send_port_desc_stats_request(datapath)
            LOG.error(_LE("No metadata on packet from %s"),
                      pkt.get_protocol(ethernet.ethernet).src)
            return

        LOG.debug("packet segmentation_id %s ", segmentation_id)

        tenant = self._get_tenant_for_msg(msg, pkt)
        if tenant is None:
            LOG.warning(_LW("No available tenant for packet %s"),
                        pkt.get_protocol(ethernet.ethernet).src)

        route = tenant.get_route(pkt)
        if route is None:
            LOG.debug(
                "No route is available for packet %(src_ip)s->%(dst_ip)s",
                {"src_ip": pkt.get_protocol(ethernet.ethernet).src,
                 "dst_ip": pkt.get_protocol(ethernet.ethernet).dst})
            return

        final_port = route[-1]
        if final_port.is_router_interface:
            self._handle_router_packet(datapath, pkt, route)
        else:
            self._handle_vm_packet(datapath, msg, pkt, route)

    def install_l3_forwarding_flows(
            self,
            datapath,
            msg,
            in_port_data,
            pkt_eth,
            pkt_ipv4,
            gateway_port_data,
            dst_port_data,
            cookie_filter,
    ):
        """Install the l3 forwarding flows.

        :param datapath: Datapath to install into
        :param msg: Message to act upon
        :param in_port_data: The port that the message arrived in
        :type in_port_data: PortData
        :param pkt_eth: The ethernet part of the packet
        :param pkt_ipv4: The ipv4 part of the packet
        :param gateway_port_data: The gateway port through which the packet
                                 would have been routed
        :type gateway_port_data: PortData
        :param dst_port_data: The destination port.
        :type dst_port_data: PortData
        :param cookie_filter: The cookie to attach to all flows
        :type cookie_filter: CookieFilter
        """
        dst_port_dp_id = dst_port_data.local_datapath_id
        dst_seg_id = dst_port_data.segmentation_id
        in_port = in_port_data.local_port_number
        dst_port = dst_port_data.local_port_number
        src_seg_id = in_port_data.segmentation_id
        cookie = cookie_filter.to_cookie()

        if dst_port_dp_id == datapath.id:
            # The dst VM and the source VM are on the same compute Node
            # Send output flow directly to port, use the same datapath
            actions = self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                in_port,
                src_seg_id,
                pkt_eth.src,
                pkt_eth.dst,
                pkt_ipv4.dst,
                pkt_ipv4.src,
                gateway_port_data.mac_address,
                dst_port_data.mac_address,
                dst_port,
                cookie=cookie,
            )
            # Install the reverse flow return traffic
            self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                dst_port,
                dst_seg_id,
                dst_port_data.mac_address,
                gateway_port_data.mac_address,
                pkt_ipv4.src,
                pkt_ipv4.dst,
                pkt_eth.dst,
                in_port_data.mac_address,
                in_port,
                cookie=cookie,
            )
            self.handle_packet_out_l3(datapath, msg, dst_port, actions)
        else:
            # The dst VM and the source VM are NOT on the same compute node
            # Send output to br-tun patch port and install reverse flow on the
            # dst compute node
            remote_switch = self.dp_list.get(dst_port_dp_id)
            local_switch = self.dp_list.get(datapath.id)
            actions = self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                in_port,
                src_seg_id,
                pkt_eth.src,
                pkt_eth.dst,
                pkt_ipv4.dst,
                pkt_ipv4.src,
                gateway_port_data.mac_address,
                dst_port_data.mac_address,
                local_switch.patch_port_num,
                dst_seg_id=dst_seg_id,
                cookie=cookie,
                dst_datapath=remote_switch.datapath,
            )

            # Remote reverse flow install
            self.add_flow_subnet_traffic(
                remote_switch.datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                dst_port,
                dst_seg_id,
                dst_port_data.mac_address,
                gateway_port_data.mac_address,
                pkt_ipv4.src,
                pkt_ipv4.dst,
                pkt_eth.dst,
                in_port_data.mac_address,
                remote_switch.patch_port_num,
                dst_seg_id=src_seg_id,
                cookie=cookie,
                dst_datapath=local_switch.datapath,
            )

            self.handle_packet_out_l3(remote_switch.datapath,
                    msg, dst_port, actions)

    def handle_packet_out_l3(self, datapath, msg, in_port, actions):
        data = None

        parser = datapath.ofproto_parser
        ofproto = datapath.ofproto
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

    def add_flow_subnet_traffic(self, datapath, table, priority, in_port,
                                src_seg_id, match_src_mac, match_dst_mac,
                                match_dst_ip, match_src_ip, src_mac,
                                dst_mac, out_port_num, dst_seg_id=None,
                                cookie=0, dst_datapath=None):
        parser = datapath.ofproto_parser
        match = parser.OFPMatch()
        match.set_dl_type(ether.ETH_TYPE_IP)
        match.set_in_port(in_port)
        match.set_metadata(src_seg_id)
        match.set_dl_src(haddr_to_bin(match_src_mac))
        match.set_dl_dst(haddr_to_bin(match_dst_mac))
        match.set_ipv4_src(ipv4_text_to_int(str(match_src_ip)))
        match.set_ipv4_dst(ipv4_text_to_int(str(match_dst_ip)))
        actions = []
        inst = []
        ofproto = datapath.ofproto
        actions.append(parser.OFPActionDecNwTtl())
        actions.append(parser.OFPActionSetField(eth_src=src_mac))
        actions.append(parser.OFPActionSetField(eth_dst=dst_mac))

        if dst_datapath:
            dst_ip_hex = self._get_dp_ip_as_int(dst_datapath)
            actions.append(parser.OFPActionSetField(reg7=dst_ip_hex))

        if dst_seg_id:
            # The dest vm is on another compute machine so we must set the
            # segmentation Id and set metadata for the tunnel bridge to
            # for this flow
            mask_dst_seg = int(dst_seg_id) | REG_32BIT_ON
            field = parser.OFPActionSetField(pkt_mark=mask_dst_seg)
            actions.append(field)
            goto_inst = parser.OFPInstructionGotoTable(
                    self.TUN_TRANSLATE_TABLE)
            inst.append(goto_inst)
        else:
            actions.append(parser.OFPActionOutput(out_port_num,
                                              ofproto.OFPCML_NO_BUFFER))
        inst.append(datapath.ofproto_parser.OFPInstructionActions(
                        ofproto.OFPIT_APPLY_ACTIONS, actions))
        self.mod_flow(
            datapath,
            cookie=cookie,
            inst=inst,<commit_msg>Rename the 32 bit mask to match REG_32BIT_ON_MASK to match kilo branch<commit_after># Copyright (c) 2015 OpenStack Foundation.
# All Rights Reserved.
#
#    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 netaddr
import struct
import threading

from ryu.controller.handler import CONFIG_DISPATCHER
from ryu.controller.handler import MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.controller import ofp_event
from ryu.ofproto import ether
from ryu.ofproto import ofproto_v1_3

from ryu.lib.packet import arp
from ryu.lib.packet import ethernet
from ryu.lib.packet import packet

from ryu.lib.mac import haddr_to_bin
from ryu.lib.packet import icmp
from ryu.lib.packet import ipv4
from ryu.lib.packet import ipv6
from ryu.lib.packet import tcp
from ryu.lib.packet import udp

from ryu.lib import addrconv

from neutron import context

from neutron.common import constants as const
from neutron.i18n import _LE, _LI, _LW
from oslo_log import log

from dragonflow.controller.df_base_app import DFlowApp
from dragonflow.utils.bloomfilter import BloomFilter
LOG = log.getLogger(__name__)

ETHERNET = ethernet.ethernet.__name__
IPV4 = ipv4.ipv4.__name__
ICMP = icmp.icmp.__name__
TCP = tcp.tcp.__name__
UDP = udp.udp.__name__

VLANID_NONE = 0
VLANID_MIN = 2
VLANID_MAX = 4094
COOKIE_SHIFT_VLANID = 32
UINT16_MAX = 0xffff
UINT32_MAX = 0xffffffff
UINT64_MAX = 0xffffffffffffffff
OFPFW_NW_PROTO = 1 << 5

REG_32BIT_ON_MASK = 0x80000000

HIGH_PRIORITY_FLOW = 1000
MEDIUM_PRIORITY_FLOW = 100
NORMAL_PRIORITY_FLOW = 10
LOW_PRIORITY_FLOW = 1
LOWEST_PRIORITY_FLOW = 0


CONTROLLER_L3_CONFIGURED_FLOW_PRIORITY = 50
ROUTER_INTERFACE_FLOW_PRIORITY = 40
LOCAL_SUBNET_TRAFFIC_FLOW_PRIORITY = 30
EAST_WEST_TRAFFIC_TO_CONTROLLER_FLOW_PRIORITY = 20
SNAT_RULES_PRIORITY_FLOW = 10


class CookieFilter(BloomFilter):
    """Bloom Filter in a cookie

    Useful and delicious!

    To get a valid int cookie:
     cf = CookieFilter(keys)
     cookie = cf.to_cookie()

    When adding a flow:
     OFPModFlow(..., cookie=cookie)

    When matching a flow (cookie can also just be 0xFFFFFFFF):
     OFPModFlow(..., cookie=cookie, cookie_mask=cookie)
    """
    def __init__(self, keys=()):
        super(CookieFilter, self).__init__(
            num_bytes=8,
            num_probes=2,
            iterable=keys,
        )

    def to_cookie(self):
        return CookieFilter._array_to_int64(self.array)

    @staticmethod
    def from_route(route):
        """Create a filter for a route

        :type route: list of PortData objects
        :rtype: CookieFilter
        """

        # TODO(saggi) memoize
        return CookieFilter(port.cookie_hash for port in route)

    @staticmethod
    def from_port_data(port_data):
        """Create a CookieFilter from a single PortData object

        :type port_data: PortData
        :rtype: CookieFilter
        """
        # TODO(saggi) memoize
        return CookieFilter((port_data.cookie_hash,))

    @staticmethod
    def _array_to_int64(array):
        """Converts bloom filter mask or state to int64

        Only works if array length is 8

        :param array: Array of a bloom filter or mask
        :return: The resulting array encoded as a signed int
        :rtype: int
        """
        assert(len(array) == 8)
        return struct.unpack("Q", str(array))[0]


class AgentDatapath(object):
    """Represents a forwarding element switch local state"""

    def __init__(self):
        self.local_ports = None
        self.datapath = 0
        self.patch_port_num = 0

        # Dictionary used to hold port information received from OVS
        # each port data structure has a link to an entry in this dictionary
        # in 'switch_port_desc'
        self.switch_port_desc_dict = {}


class TenantTopology(object):
    """Represents a tenant topology"""

    def __init__(self, tenant_id):
        self.routers = {}
        self.mac_to_port_data = {}
        self.subnets = {}
        self.id = tenant_id

    def add_router(self, router):
        self.routers[router.id] = router

    def del_router(self, id):
        try:
            del self.routers[id]
        except KeyError:
            return -1

    def find_port_data_by_ip_address(self, ip_address):
        for port_data in self.mac_to_port_data.values():
            for fixed_ip in port_data.fixed_ips:
                if ip_address == fixed_ip['ip_address']:
                    return port_data, self.subnets[fixed_ip['subnet_id']]
        else:
            return 0, 0

    def find_port_data_by_local_name(self, local_port_name):
        """
        :param local_port_name: local name of the port on the switch
                                (eg. "qvo2dcdg-as")
        :type: local_port_name: str
        :return: A PortData object if one was found or None if no matching
                 object exists
        :type: PortData
        """

        name_prefix_len = 3
        partial_id = local_port_name[name_prefix_len:]
        for port_data in self.mac_to_port_data.values():
            if port_data.id.startswith(partial_id):
                return port_data
        else:
            return None

    def get_route(self, pkt):
        """Get a possible route the packet can take to reach it's destination

        The are no guarantees that the returned route is the fastest. Only that
        it's possible.

        Currently doesn't support extra routes.

        :param pkt: The packet to trace
        :return: A list of port_data objects if a route was found or None if no
                 route is available.
        """
        pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
        pkt_ethernet = pkt.get_protocol(ethernet.ethernet)

        in_port_data = self.mac_to_port_data.get(pkt_ethernet.src)
        out_port_data = self.mac_to_port_data.get(pkt_ethernet.dst)
        if in_port_data is None or out_port_data is None:
            LOG.error(
                _LE("No data for packet ports %(src_mac)s %(dst_mac)s"),
                {"src_mac": pkt_ethernet.src,
                 "dst_mac": pkt_ethernet.dst}
            )
            return None

        (dst_port_data, dst_subnet) = self.find_port_data_by_ip_address(
            pkt_ipv4.dst
        )
        if not dst_port_data:
            LOG.error(
                _LE("No data for destination port %(dst_ip)s"),
                {"dst_ip": pkt_ipv4.dst}
            )
            return None

        is_same_port = out_port_data.id == dst_port_data.id
        if is_same_port:
            return [in_port_data, out_port_data]
        # In order to hop the target has to be a router
        if not out_port_data.is_router_interface:
            LOG.error(
                _LE("The gateway port is not a router  %(dst_mac)s"),
                {"dst_mac": pkt_ethernet.dst}
            )
            return None

        gateway_router = self.routers.get(out_port_data.device_id)
        if gateway_router is None:
            return None

        if dst_subnet.id in gateway_router.subnets:
            # TODO(saggi) add second leg to route
            return [in_port_data, out_port_data, dst_port_data]

        # route not found
        return None

    @property
    def unused_subnets(self):
        unused_subnets = self.subnets.copy()
        for port in self.mac_to_port_data.values():
            for fixed_ip in port.fixed_ips:
                unused_subnets.pop(fixed_ip['subnet_id'], None)

            # Optimization, no need to keep filtering if it's empty
            if len(unused_subnets) == 0:
                break

        return unused_subnets.values()

    def get_routers_ports_by_subnet(self, subnet):
        ports = []
        try:
            for router in self.routers.values():
                for subnet_id in router.subnets:
                    for interface in router.interfaces:
                        for subnet_info in interface['subnets']:
                            if subnet.id == subnet_info['id']:
                                ports.append(interface)

        except KeyError:
            LOG.error(_LE("AttributeError: for %s"), subnet.id)

        return ports


class Router(object):

    def __init__(self, data):
        self.data = data
        self.subnets = []

    def add_subnet(self, subnet):
        self.subnets.append(subnet.id)

    def remove_subnet(self, subnet):
        self.subnets.remove(subnet.id)

    @property
    def id(self):
        return self.data['id']

    @property
    def interfaces(self):
        return self.data.get('_interfaces', ())


class Subnet(object):

    def __init__(self, data, segmentation_id):
        self.data = data
        self.segmentation_id = segmentation_id

    def set_data(self, data):
        self.data = data

    @property
    def id(self):
        try:
            return self.data['id']
        except TypeError:
            return -1

    @property
    def cidr(self):
        """Return the CIDR information for this subnet

        :return: CIDR information for subnet
        :rtype: netaddr.IPNetwork
        """
        return netaddr.IPNetwork(self.data['cidr'])

    @property
    def gateway_ip(self):
        return self.data.get('gateway_ip')

    def is_ipv4(self):
        try:
            return (netaddr.IPNetwork(self.cidr).ip).version == 4
        except TypeError:
            return False

    def __repr__(self):
        return ("<Subnet id='%s' cidr='%s' gateway_ip='%s' " +
                "segmentation_id='%s'>") % (
            self.id,
            self.cidr,
            self.gateway_ip,
            self.segmentation_id,
        )


class SnatBinding(object):

    def __init__(self, subnet, port):
        self.subnet_id = subnet
        self.sn_port = port
        self.segmentation_id = 0


class PortData(object):
    def __init__(self, port_data):
        self._port_data = port_data

    @property
    def is_router_interface(self):
        if self._port_data['device_owner'] in const.ROUTER_INTERFACE_OWNERS:
            return True
        else:
            return False

    @property
    def id(self):
        return self._port_data['id']

    @property
    def device_id(self):
        return self._port_data['device_id']

    @property
    def fixed_ips(self):
        try:
            return tuple(self._port_data.get('fixed_ips'))
        except KeyError:
            return tuple()

    @property
    def subnets(self):
        subnets = []
        for fixed_ip in self.fixed_ips:
            try:
                subnets.append(fixed_ip['subnet_id'])
            except KeyError:
                LOG.error(_LE("No subnet info for %s"), fixed_ip)
                continue
        return subnets

    @property
    def segmentation_id(self):
        return self._port_data['segmentation_id']

    @property
    def local_port_number(self):
        try:
            return self._port_data['switch_port_desc']['local_port_num']
        except KeyError:
            return -1

    @property
    def local_datapath_id(self):
        try:
            return self._port_data['switch_port_desc']['local_dpid_switch']
        except KeyError:
            return -1

    @property
    def mac_address(self):
        return self._port_data['mac_address']

    @property
    def cookie_hash(self):
        """Create an int64 to be used for ovs cookie needs.
        :return:
        :rtype: int
        """
        uuid_prefix = self.id.split('-', 1)[0]
        return int(uuid_prefix, 16)

    def get_subnet_from_ip_address(self, ip_address):
        for fixed_ip in self.fixed_ips:
            if ip_address == fixed_ip['ip_address']:
                return fixed_ip
        else:
            return None

    @property
    def is_gateway(self):
        for subnet in self._port_data['subnets']:
            for fixed_ip in self.fixed_ips:
                if subnet['id'] == fixed_ip['subnet_id']:
                    if subnet['gateway_ip'] == fixed_ip['ip_address']:
                        return True
        else:
            return False

    def __getitem__(self, item):
        return self._port_data.__getitem__(item)

    def __setitem__(self, key, value):
        return self._port_data.__setitem__(key, value)


class L3ReactiveApp(DFlowApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]
    BASE_RPC_API_VERSION = '1.0'

    BASE_TABLE = 0
    CLASSIFIER_TABLE = 40
    METADATA_TABLE_ID = 50
    ARP_AND_BR_TABLE = 51
    L3_VROUTER_TABLE = 52
    L3_PUBLIC_TABLE = 53
    TUN_TRANSLATE_TABLE = 60

    def __init__(self, *args, **kwargs):
        super(L3ReactiveApp, self).__init__(*args, **kwargs)
        self.mac_to_port = {}

        self.ctx = context.get_admin_context()
        self.lock = threading.Lock()
        self._tenants = {}
        self.need_sync = True
        self.dp_list = {}
        self.snat_bindings = {}
        self.idle_timeout = kwargs['idle_timeout']
        self.hard_timeout = kwargs['hard_timeout']

    def get_tenant_by_id(self, tenant_id):
        if tenant_id in self._tenants:
            return self._tenants[tenant_id]
        else:
            return self._tenants.setdefault(tenant_id,
                                            TenantTopology(tenant_id))

    def start(self):
        LOG.info(_LI("Starting Virtual L3 Reactive OpenFlow APP "))
        super(L3ReactiveApp, self).start()
        return 1

    def delete_router(self, router_id):
        for tenant in self._tenants.values():
            try:
                router = tenant.routers.pop(router_id)
            except KeyError:
                pass
            else:
                for interface in router.interfaces:
                    for subnet_info in interface['subnets']:
                        subnet = tenant.subnets[subnet_info['id']]
                        if subnet.segmentation_id == 0:
                            continue

                        if subnet.is_ipv4():
                            self._remove_vrouter_arp_responder_cast(
                                subnet.segmentation_id,
                                interface['mac_address'],
                                self.get_ip_from_interface(interface))

    def sync_router(self, router_info):
        LOG.info(_LI("sync_router --> %s"), router_info)
        tenant_topology = self.get_tenant_by_id(router_info['tenant_id'])

        router = Router(router_info)
        router_old = tenant_topology.routers.get(router.id)
        tenant_topology.add_router(router)
        subnets = tenant_topology.subnets

        for interface in router.interfaces:
            for subnet_info in interface['subnets']:
                subnet = subnets.setdefault(
                        subnet_info['id'],
                        Subnet(subnet_info, 0),
                )
                if subnet.data is None:
                    subnet.set_data(subnet_info)

                router.add_subnet(subnet)
                if subnet.segmentation_id != 0:
                    self.subnet_added_binding_cast(subnet, interface)
                    self.bootstrap_network_classifiers(
                        subnet=subnet)

        # If previous definition of the router is known
        if router_old:
            # Handle removed subnets
            for interface in router_old.interfaces:
                for subnet_info in interface['subnets']:
                    subnet = subnets[subnet_info['id']]
                    if subnet.segmentation_id == 0:
                        continue

                    # if subnet was not deleted
                    if subnet.id in router.subnets:
                        continue

                    if subnet.is_ipv4():
                        self._remove_vrouter_arp_responder_cast(
                            subnet.segmentation_id,
                            interface['mac_address'],
                            self.get_ip_from_interface(interface))
                        if PortData(interface).is_gateway:
                            self._handle_remove_subnet(subnet)

                        self._handle_remove_port(PortData(interface))

    def _handle_remove_subnet(self, subnet):
        """Remove all the flow relating to a specific subnet

        :param subnet:
        :type subnet: Subnet
        """
        for dp in self.dp_list.values():
            datapath = dp.datapath
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_ipv4_dst_masked(subnet.cidr.ip.value,
                                      subnet.cidr.netmask.value)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.L3_VROUTER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_metadata(subnet.segmentation_id)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.L3_VROUTER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)

    def attach_switch_port_desc_to_port_data(self, port_data):
        if 'id' in port_data:
            port_id = port_data['id']
            sub_str_port_id = str(port_id[0:11])

            # Only true if we already received port_desc from OVS
            for switch in self.dp_list.values():
                switch_port_desc_dict = switch.switch_port_desc_dict
                if sub_str_port_id in switch_port_desc_dict:
                    port_data['switch_port_desc'] = (
                        switch_port_desc_dict[sub_str_port_id])
                    port_desc = port_data['switch_port_desc']
                    self.add_flow_metadata_by_port_num(
                        port_desc['datapath'],
                        0,
                        HIGH_PRIORITY_FLOW,
                        port_desc['local_port_num'],
                        port_data['segmentation_id'],
                        0xffff,
                        self.CLASSIFIER_TABLE)

    def delete_port(self, port):
        """

        :param port:
        :type port: PortData
        """
        self._handle_remove_port(PortData(port))

    def _remove_flow_local_subnet(self, subnet):
        """

        :param subnet:
        :type subnet: Subnet
        """
        for dp in self.dp_list.values():
            datapath = dp.datapath
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            match = parser.OFPMatch()
            match.set_dl_type(ether.ETH_TYPE_IP)
            match.set_metadata(subnet.segmentation_id)
            msg = parser.OFPFlowMod(datapath=datapath,
                                    cookie=0,
                                    cookie_mask=0,
                                    table_id=L3ReactiveApp.CLASSIFIER_TABLE,
                                    command=ofproto.OFPFC_DELETE,
                                    priority=MEDIUM_PRIORITY_FLOW,
                                    out_port=ofproto.OFPP_ANY,
                                    out_group=ofproto.OFPG_ANY,
                                    match=match)
            datapath.send_msg(msg)

    def _find_tenant_for_port_data(self, port_data):
        """

        :param port_data:
        :type port_data: PortData
        :return:
        :rtype: TenantTopology or None
        """

        for tenant in self._tenants.values():
            if port_data.mac_address in tenant.mac_to_port_data:
                return tenant
        else:
            return None

    def sync_port(self, port):
        LOG.info(_LI("sync_port--> %s\n"), port)
        segmentation_id = port.get('segmentation_id')
        if segmentation_id is None:
            LOG.info(_LI("no segmentation data in port --> %s"), port)
            return

        tenant_topo = self.get_tenant_by_id(port['tenant_id'])
        for subnet_dict in port.get('subnets', []):
            subnet = tenant_topo.subnets.get(subnet_dict['id'])
            if subnet:
                subnet.set_data(subnet_dict)
                subnet.segmentation_id = segmentation_id
            else:
                tenant_topo.subnets[subnet_dict['id']] = subnet = Subnet(
                    subnet_dict,
                    segmentation_id)
            if port['device_owner'] == const.DEVICE_OWNER_ROUTER_INTF:
                self.subnet_added_binding_cast(subnet, port)
                self.bootstrap_network_classifiers(subnet=subnet)
            self._add_flow_normal_local_subnet_cast(
                    LOCAL_SUBNET_TRAFFIC_FLOW_PRIORITY,
                    subnet)
        tenant_topo.mac_to_port_data[port['mac_address']] = PortData(port)
        self.attach_switch_port_desc_to_port_data(port)

    def _get_input_packet_handler(self, pkt):
        is_ipv4_packet = pkt.get_protocol(ipv4.ipv4) is not None
        is_ipv6_packet = pkt.get_protocol(ipv6.ipv6) is not None

        packet_handler = None
        if is_ipv4_packet:
            packet_handler = self.handle_ipv4_packet_in

        elif is_ipv6_packet:
            packet_handler = self.handle_ipv6_packet_in

        return packet_handler

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def OF_packet_in_handler(self, event):
        msg = event.msg
        datapath = msg.datapath
        ofproto = datapath.ofproto

        if msg.reason == ofproto.OFPR_NO_MATCH:
            reason = 'NO MATCH'
        elif msg.reason == ofproto.OFPR_ACTION:
            reason = 'ACTION'
        elif msg.reason == ofproto.OFPR_INVALID_TTL:
            reason = 'INVALID TTL'
        else:
            reason = 'unknown'

        LOG.debug('OFPPacketIn received: '
                  'buffer_id=%x total_len=%d reason=%s '
                  'table_id=%d cookie=%d match=%s',
                  msg.buffer_id, msg.total_len, reason,
                  msg.table_id, msg.cookie, msg.match)

        pkt = packet.Packet(msg.data)
        packet_handler = self._get_input_packet_handler(pkt)
        if packet_handler is None:
            LOG.error(_LE("Unable to find appropriate packet "
                          "handler for packet: %s"), pkt)
        else:
            try:
                packet_handler(datapath, msg, pkt)
            except Exception as exception:
                LOG.debug(
                    "Unable to handle packet %(msg)s: %(e)s",
                    {'msg': msg, 'e': exception}
                )

    def handle_ipv6_packet_in(self, datapath, msg, pkt):
        # TODO(gampel)(gampel) add ipv6 support
        LOG.error(_LE("No handle for ipv6 yet should be offload to the"
                "NORMAL path  %s"), pkt)
        return

    def is_known_datapath(self, datapath):
        """Check if datapath is known to this openflow appliaction"""
        return self.dp_list.get(datapath.id) is not None

    def _handle_router_packet(self, datapath, pkt, route):
        """Handle packets intended for routers

        Specifically OAM (Operations, administration and management) packets
        Does nothing if pkt is not a supported protocol.

        The function assumes the route is valid and the packet is meant for the
        last port in the route.

        Currently only handles ping

        :param datapath: The datapath to send through
        :param pkt: The packet to handle
        :param route: The resolved route the packet is it take
        """
        is_icmp_packet = pkt.get_protocol(icmp.icmp) is not None

        if is_icmp_packet:
            # send ping response
            self._handle_icmp(
                datapath,
                pkt,
                route[0],
            )

        else:
            pkt_ipv4 = pkt.get_protocol(ipv4.ipv4) is not None
            LOG.error(_LE("any communication to a router that "
                          "is not ping should be dropped from "
                          "ip '%s'"),
                      pkt_ipv4.src)

    def _handle_vm_packet(self, datapath, msg, pkt, route):
        """Handle packets intended for VMs
        """
        pkt_ipv4 = pkt.get_protocol(ipv4.ipv4)
        pkt_ethernet = pkt.get_protocol(ethernet.ethernet)

        LOG.debug(
            "Installing flow Route %s-> %s",
            pkt_ipv4.src,
            pkt_ipv4.dst)

        self.install_l3_forwarding_flows(
            datapath,
            msg,
            route[0],
            pkt_ethernet,
            pkt_ipv4,
            route[1],
            route[-1],
            CookieFilter.from_route(route),
        )

    def _get_tenant_for_msg(self, msg, pkt):
        segmentation_id = msg.match.get('metadata')
        if segmentation_id is None:
            return None

        pkt_eth = pkt.get_protocol(ethernet.ethernet)
        for tenant in self._tenants.values():
            port_data = tenant.mac_to_port_data.get(pkt_eth)
            if port_data is None:
                # target is router
                for subnet in tenant.subnets.values():
                    if subnet.segmentation_id == segmentation_id:
                        return tenant
            else:
                for fixed_ip in port_data.fixed_ips:
                    subnet = tenant.subnets.get(fixed_ip['subnet_id'])
                    if subnet is None:
                        continue

                    if subnet.segmentation_id == segmentation_id:
                        return tenant

    def handle_ipv4_packet_in(self, datapath, msg, pkt):
        if not self.is_known_datapath(datapath):
            LOG.warning(_LW("Received packet from unknown datapath '%s'"),
                        datapath.id)
            return

        segmentation_id = msg.match.get('metadata')
        if segmentation_id is None:
            # send request for local switch data
            self.send_port_desc_stats_request(datapath)
            LOG.error(_LE("No metadata on packet from %s"),
                      pkt.get_protocol(ethernet.ethernet).src)
            return

        LOG.debug("packet segmentation_id %s ", segmentation_id)

        tenant = self._get_tenant_for_msg(msg, pkt)
        if tenant is None:
            LOG.warning(_LW("No available tenant for packet %s"),
                        pkt.get_protocol(ethernet.ethernet).src)

        route = tenant.get_route(pkt)
        if route is None:
            LOG.debug(
                "No route is available for packet %(src_ip)s->%(dst_ip)s",
                {"src_ip": pkt.get_protocol(ethernet.ethernet).src,
                 "dst_ip": pkt.get_protocol(ethernet.ethernet).dst})
            return

        final_port = route[-1]
        if final_port.is_router_interface:
            self._handle_router_packet(datapath, pkt, route)
        else:
            self._handle_vm_packet(datapath, msg, pkt, route)

    def install_l3_forwarding_flows(
            self,
            datapath,
            msg,
            in_port_data,
            pkt_eth,
            pkt_ipv4,
            gateway_port_data,
            dst_port_data,
            cookie_filter,
    ):
        """Install the l3 forwarding flows.

        :param datapath: Datapath to install into
        :param msg: Message to act upon
        :param in_port_data: The port that the message arrived in
        :type in_port_data: PortData
        :param pkt_eth: The ethernet part of the packet
        :param pkt_ipv4: The ipv4 part of the packet
        :param gateway_port_data: The gateway port through which the packet
                                 would have been routed
        :type gateway_port_data: PortData
        :param dst_port_data: The destination port.
        :type dst_port_data: PortData
        :param cookie_filter: The cookie to attach to all flows
        :type cookie_filter: CookieFilter
        """
        dst_port_dp_id = dst_port_data.local_datapath_id
        dst_seg_id = dst_port_data.segmentation_id
        in_port = in_port_data.local_port_number
        dst_port = dst_port_data.local_port_number
        src_seg_id = in_port_data.segmentation_id
        cookie = cookie_filter.to_cookie()

        if dst_port_dp_id == datapath.id:
            # The dst VM and the source VM are on the same compute Node
            # Send output flow directly to port, use the same datapath
            actions = self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                in_port,
                src_seg_id,
                pkt_eth.src,
                pkt_eth.dst,
                pkt_ipv4.dst,
                pkt_ipv4.src,
                gateway_port_data.mac_address,
                dst_port_data.mac_address,
                dst_port,
                cookie=cookie,
            )
            # Install the reverse flow return traffic
            self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                dst_port,
                dst_seg_id,
                dst_port_data.mac_address,
                gateway_port_data.mac_address,
                pkt_ipv4.src,
                pkt_ipv4.dst,
                pkt_eth.dst,
                in_port_data.mac_address,
                in_port,
                cookie=cookie,
            )
            self.handle_packet_out_l3(datapath, msg, dst_port, actions)
        else:
            # The dst VM and the source VM are NOT on the same compute node
            # Send output to br-tun patch port and install reverse flow on the
            # dst compute node
            remote_switch = self.dp_list.get(dst_port_dp_id)
            local_switch = self.dp_list.get(datapath.id)
            actions = self.add_flow_subnet_traffic(
                datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                in_port,
                src_seg_id,
                pkt_eth.src,
                pkt_eth.dst,
                pkt_ipv4.dst,
                pkt_ipv4.src,
                gateway_port_data.mac_address,
                dst_port_data.mac_address,
                local_switch.patch_port_num,
                dst_seg_id=dst_seg_id,
                cookie=cookie,
                dst_datapath=remote_switch.datapath,
            )

            # Remote reverse flow install
            self.add_flow_subnet_traffic(
                remote_switch.datapath,
                self.L3_VROUTER_TABLE,
                MEDIUM_PRIORITY_FLOW,
                dst_port,
                dst_seg_id,
                dst_port_data.mac_address,
                gateway_port_data.mac_address,
                pkt_ipv4.src,
                pkt_ipv4.dst,
                pkt_eth.dst,
                in_port_data.mac_address,
                remote_switch.patch_port_num,
                dst_seg_id=src_seg_id,
                cookie=cookie,
                dst_datapath=local_switch.datapath,
            )

            self.handle_packet_out_l3(remote_switch.datapath,
                    msg, dst_port, actions)

    def handle_packet_out_l3(self, datapath, msg, in_port, actions):
        data = None

        parser = datapath.ofproto_parser
        ofproto = datapath.ofproto
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data
        out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
                                  in_port=in_port, actions=actions, data=data)
        datapath.send_msg(out)

    def add_flow_subnet_traffic(self, datapath, table, priority, in_port,
                                src_seg_id, match_src_mac, match_dst_mac,
                                match_dst_ip, match_src_ip, src_mac,
                                dst_mac, out_port_num, dst_seg_id=None,
                                cookie=0, dst_datapath=None):
        parser = datapath.ofproto_parser
        match = parser.OFPMatch()
        match.set_dl_type(ether.ETH_TYPE_IP)
        match.set_in_port(in_port)
        match.set_metadata(src_seg_id)
        match.set_dl_src(haddr_to_bin(match_src_mac))
        match.set_dl_dst(haddr_to_bin(match_dst_mac))
        match.set_ipv4_src(ipv4_text_to_int(str(match_src_ip)))
        match.set_ipv4_dst(ipv4_text_to_int(str(match_dst_ip)))
        actions = []
        inst = []
        ofproto = datapath.ofproto
        actions.append(parser.OFPActionDecNwTtl())
        actions.append(parser.OFPActionSetField(eth_src=src_mac))
        actions.append(parser.OFPActionSetField(eth_dst=dst_mac))

        if dst_datapath:
            dst_ip_hex = self._get_dp_ip_as_int(dst_datapath)
            actions.append(parser.OFPActionSetField(reg7=dst_ip_hex))

        if dst_seg_id:
            # The dest vm is on another compute machine so we must set the
            # segmentation Id and set metadata for the tunnel bridge to
            # for this flow
            mask_dst_seg = int(dst_seg_id) | REG_32BIT_ON_MASK
            field = parser.OFPActionSetField(pkt_mark=mask_dst_seg)
            actions.append(field)
            goto_inst = parser.OFPInstructionGotoTable(
                    self.TUN_TRANSLATE_TABLE)
            inst.append(goto_inst)
        else:
            actions.append(parser.OFPActionOutput(out_port_num,
                                              ofproto.OFPCML_NO_BUFFER))
        inst.append(datapath.ofproto_parser.OFPInstructionActions(
                        ofproto.OFPIT_APPLY_ACTIONS, actions))
        self.mod_flow(
            datapath,
            cookie=cookie,
            inst=inst,
{'fraction_non_alphanumeric': 0.0749049702359607, 'fraction_numerical': 0.006110593129168758, 'mean_word_length': 1.7380410022779043, 'pattern_counts': {'":': 12, '<': 9, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 8, 'word_list_counts': {'cursed_substrings.json': 64, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7136889', 'n_tokens_mistral': 21086, 'n_tokens_neox': 20300, 'n_words': 4430}