# Copyright (C) 2014 VA Linux Systems Japan K.K.
# Copyright (C) 2014 YAMAMOTO Takashi <yamamoto at valinux co jp>
# 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.

"""
OpenFlow1.3 flow table for OFAgent

* requirements
** plain OpenFlow 1.3. no vendor extensions.

* legends
 xxx: network id  (agent internal use)
 yyy: segment id  (vlan id, gre key, ...)
 a: tunnel port  (tun_ofports)
 i,j,k: vm port  (map[net_id].vif_ports[vif_id].ofport)
 x,y,z: physical port  (int_ofports)
 N: tunnel type  (0 for TYPE_GRE, 1 for TYPE_xxx, ...)
 iii: unknown ip address
 uuu: unicast l2 address
 rrr, sss, ttt: tunnel peer's ip address

* tables (in order)
    CHECK_IN_PORT
    TUNNEL_IN+N
    PHYS_IN
    LOCAL_IN
    ARP_PASSTHROUGH
    ARP_RESPONDER
    TUNNEL_OUT
    LOCAL_OUT
    PHYS_OUT
    TUNNEL_FLOOD+N
    PHYS_FLOOD
    LOCAL_FLOOD

* CHECK_IN_PORT

   for each vm ports:
      // check_in_port_add_local_port, check_in_port_delete_port
      in_port=i, write_metadata(LOCAL|xxx),goto(LOCAL_IN)
   TYPE_GRE
   for each tunnel ports:
      // check_in_port_add_tunnel_port, check_in_port_delete_port
      in_port=a,tun_ipv4_dst=local_ip, goto(TUNNEL_IN+N)
   TYPE_VLAN
   for each networks ports:
      // provision_tenant_physnet, reclaim_tenant_physnet
      in_port=x,vlan_vid=present|yyy, write_metadata(xxx),goto(PHYS_IN)
   TYPE_FLAT
      // provision_tenant_physnet, reclaim_tenant_physnet
      in_port=x, write_metadata(xxx),goto(PHYS_IN)
   default drop

* TUNNEL_IN+N  (per tunnel types)  tunnel -> network

   for each networks:
      // provision_tenant_tunnel, reclaim_tenant_tunnel
      tun_id=yyy, write_metadata(xxx),goto(TUNNEL_OUT)

   default drop

* PHYS_IN
   default goto(TUNNEL_OUT)

* LOCAL_IN
   default goto(next_table)

* ARP_PASSTHROUGH
   for each unknown tpa:
      // arp_passthrough
      arp,arp_op=request,metadata=xxx,tpa=iii, idle_timeout=5, goto(TUNNEL_OUT)
   default goto(next_table)

* ARP_RESPONDER
   arp,arp_op=request, output:controller
   default goto(next_table)

* TUNNEL_OUT
   TYPE_GRE
   // !FLOODING_ENTRY
   // install_tunnel_output, delete_tunnel_output
   metadata=LOCAL|xxx,eth_dst=uuu  set_field:rrr->tun_ipv4_dst, \
                                   set_tunnel(yyy),output:a

   default goto(next table)

* LOCAL_OUT
   for each known destinations:
      // local_out_add_port, local_out_delete_port
      metadata=xxx,eth_dst=uuu output:i
   default goto(next table)

* PHYS_OUT

   NOTE(yamamoto): currently this table is always empty.

   default goto(next table)

* TUNNEL_FLOOD+N. (per tunnel types)

   network -> tunnel/vlan
   output to tunnel/physical ports
   "next table" might be LOCAL_OUT
   TYPE_GRE
   for each networks:
      // FLOODING_ENTRY
      // install_tunnel_output, delete_tunnel_output
      metadata=LOCAL|xxx, set_tunnel(yyy),\
                          set_field:rrr->tun_ipv4_dst,output:a, \
                          set_field:sss->tun_ipv4_dst,output:a, \
                          set_field:ttt->tun_ipv4_dst,output:a, \
                          goto(next table)

   default goto(next table)

* PHYS_FLOOD

   TYPE_VLAN
   for each networks:
      // provision_tenant_physnet, reclaim_tenant_physnet
      metadata=LOCAL|xxx, push_vlan:0x8100,set_field:present|yyy->vlan_vid,
                    output:x,pop_vlan,goto(next table)
   TYPE_FLAT
   for each networks:
      // provision_tenant_physnet, reclaim_tenant_physnet
      metadata=LOCAL|xxx, output:x,goto(next table)

   default goto(next table)

* LOCAL_FLOOD

   for each networks:
      // local_flood_update, local_flood_delete
      metadata=xxx, output:i,j,k
      or
      metadata=xxx,eth_dst=broadcast, output:i,j,k

   default drop

* references
** OVS agent https://wiki.openstack.org/wiki/Ovs-flow-logic
*** we use metadata instead of "internal" VLANs
*** we don't want to use NX learn action
"""

import itertools

from ryu.lib.packet import arp
from ryu.ofproto import ether

from neutron.plugins.common import constants as p_const

import networking_ofagent.plugins.ofagent.agent.metadata as meta
from networking_ofagent.plugins.ofagent.agent import ofswitch
from networking_ofagent.plugins.ofagent.agent import tables


class OFAgentIntegrationBridge(ofswitch.OpenFlowSwitch):
    """ofagent br-int specific logic."""

    def setup_default_table(self):
        self.delete_flows()

        self.install_default_drop(tables.CHECK_IN_PORT)

        for t in tables.TUNNEL_IN.values():
            self.install_default_drop(t)
        self.install_default_goto(tables.PHYS_IN, tables.TUNNEL_OUT)
        self.install_default_goto_next(tables.LOCAL_IN)
        self.install_default_goto_next(tables.ARP_PASSTHROUGH)
        self.install_arp_responder(tables.ARP_RESPONDER)

        self.install_default_goto_next(tables.TUNNEL_OUT)
        self.install_default_goto_next(tables.LOCAL_OUT)
        self.install_default_goto_next(tables.PHYS_OUT)

        for t in tables.TUNNEL_FLOOD.values():
            self.install_default_goto_next(t)
        self.install_default_goto_next(tables.PHYS_FLOOD)
        self.install_default_drop(tables.LOCAL_FLOOD)

    def install_arp_responder(self, table_id):
        (dp, ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(eth_type=ether.ETH_TYPE_ARP,
                              arp_op=arp.ARP_REQUEST)
        actions = [ofpp.OFPActionOutput(ofp.OFPP_CONTROLLER)]
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=table_id,
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)
        self.install_default_goto_next(table_id)

    def install_tunnel_output(self, table_id,
                              network, segmentation_id,
                              port, remote_ips,
                              goto_next, **additional_matches):
        (dp, ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(metadata=meta.mk_metadata(network, meta.LOCAL),
                              **additional_matches)
        actions = [ofpp.OFPActionSetField(tunnel_id=segmentation_id)]
        actions += itertools.chain.from_iterable([[
            ofpp.OFPActionSetField(tun_ipv4_dst=ip),
            ofpp.OFPActionOutput(port=port)
        ] for ip in remote_ips])
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions),
        ]
        if goto_next:
            instructions += [
                ofpp.OFPInstructionGotoTable(table_id=table_id + 1),
            ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=table_id,
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def delete_tunnel_output(self, table_id,
                             network, **additional_matches):
        (dp, _ofp, ofpp) = self._get_dp()
        self.delete_flows(table_id=table_id,
                          metadata=meta.mk_metadata(network, meta.LOCAL),
                          **additional_matches)

    def provision_tenant_tunnel(self, network_type, network, segmentation_id):
        (dp, _ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(tunnel_id=segmentation_id)
        metadata = meta.mk_metadata(network)
        instructions = [
            ofpp.OFPInstructionWriteMetadata(metadata=metadata[0],
                                             metadata_mask=metadata[1]),
            ofpp.OFPInstructionGotoTable(table_id=tables.TUNNEL_OUT),
        ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.TUNNEL_IN[network_type],
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def reclaim_tenant_tunnel(self, network_type, network, segmentation_id):
        table_id = tables.TUNNEL_IN[network_type]
        self.delete_flows(table_id=table_id, tunnel_id=segmentation_id)

    def provision_tenant_physnet(self, network_type, network,
                                 segmentation_id, phys_port):
        """for vlan and flat."""
        assert(network_type in [p_const.TYPE_VLAN, p_const.TYPE_FLAT])
        (dp, ofp, ofpp) = self._get_dp()

        # inbound
        metadata = meta.mk_metadata(network)
        instructions = [
            ofpp.OFPInstructionWriteMetadata(metadata=metadata[0],
                                             metadata_mask=metadata[1])
        ]
        if network_type == p_const.TYPE_VLAN:
            vlan_vid = segmentation_id | ofp.OFPVID_PRESENT
            match = ofpp.OFPMatch(in_port=phys_port, vlan_vid=vlan_vid)
            actions = [ofpp.OFPActionPopVlan()]
            instructions += [ofpp.OFPInstructionActions(
                             ofp.OFPIT_APPLY_ACTIONS, actions)]
        else:
            match = ofpp.OFPMatch(in_port=phys_port)
        instructions += [ofpp.OFPInstructionGotoTable(table_id=tables.PHYS_IN)]
        msg = ofpp.OFPFlowMod(dp,
                              priority=1,
                              table_id=tables.CHECK_IN_PORT,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

        # outbound
        match = ofpp.OFPMatch(metadata=meta.mk_metadata(network, meta.LOCAL))
        if network_type == p_const.TYPE_VLAN:
            actions = [
                ofpp.OFPActionPushVlan(),
                ofpp.OFPActionSetField(vlan_vid=vlan_vid),
            ]
        else:
            actions = []
        actions += [ofpp.OFPActionOutput(port=phys_port)]
        if network_type == p_const.TYPE_VLAN:
            actions += [ofpp.OFPActionPopVlan()]
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions),
            ofpp.OFPInstructionGotoTable(table_id=tables.PHYS_FLOOD + 1),
        ]
        msg = ofpp.OFPFlowMod(dp,
                              priority=1,
                              table_id=tables.PHYS_FLOOD,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def reclaim_tenant_physnet(self, network_type, network,
                               segmentation_id, phys_port):
        (_dp, ofp, _ofpp) = self._get_dp()
        vlan_vid = segmentation_id | ofp.OFPVID_PRESENT
        if network_type == p_const.TYPE_VLAN:
            self.delete_flows(table_id=tables.CHECK_IN_PORT,
                              in_port=phys_port, vlan_vid=vlan_vid)
        else:
            self.delete_flows(table_id=tables.CHECK_IN_PORT,
                              in_port=phys_port)
        self.delete_flows(table_id=tables.PHYS_FLOOD,
                          metadata=meta.mk_metadata(network))

    def check_in_port_add_tunnel_port(self, network_type, port, local_ip):
        (dp, _ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(in_port=port, tun_ipv4_dst=local_ip)
        instructions = [
            ofpp.OFPInstructionGotoTable(
                table_id=tables.TUNNEL_IN[network_type])
        ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.CHECK_IN_PORT,
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def check_in_port_add_local_port(self, network, port):
        (dp, ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(in_port=port)
        metadata = meta.mk_metadata(network, meta.LOCAL)
        instructions = [
            ofpp.OFPInstructionWriteMetadata(metadata=metadata[0],
                                             metadata_mask=metadata[1]),
            ofpp.OFPInstructionGotoTable(table_id=tables.LOCAL_IN),
        ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.CHECK_IN_PORT,
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def check_in_port_delete_port(self, port):
        self.delete_flows(table_id=tables.CHECK_IN_PORT, in_port=port)

    def local_flood_update(self, network, ports, flood_unicast):
        (dp, ofp, ofpp) = self._get_dp()
        match_all = ofpp.OFPMatch(metadata=meta.mk_metadata(network))
        match_multicast = ofpp.OFPMatch(metadata=meta.mk_metadata(network),
                                        eth_dst=('01:00:00:00:00:00',
                                                 '01:00:00:00:00:00'))
        if flood_unicast:
            match_add = match_all
            match_del = match_multicast
        else:
            match_add = match_multicast
            match_del = match_all
        actions = [ofpp.OFPActionOutput(port=p) for p in ports]
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions),
        ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.LOCAL_FLOOD,
                              priority=1,
                              match=match_add,
                              instructions=instructions)
        self._send_msg(msg)
        self.delete_flows(table_id=tables.LOCAL_FLOOD, strict=True,
                          priority=1, match=match_del)

    def local_flood_delete(self, network):
        self.delete_flows(table_id=tables.LOCAL_FLOOD,
                          metadata=meta.mk_metadata(network))

    def local_out_add_port(self, network, port, mac):
        (dp, ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(metadata=meta.mk_metadata(network), eth_dst=mac)
        actions = [ofpp.OFPActionOutput(port=port)]
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions),
        ]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.LOCAL_OUT,
                              priority=1,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)

    def local_out_delete_port(self, network, mac):
        self.delete_flows(table_id=tables.LOCAL_OUT,
                          metadata=meta.mk_metadata(network), eth_dst=mac)

    def arp_passthrough(self, network, tpa):
        (dp, ofp, ofpp) = self._get_dp()
        match = ofpp.OFPMatch(metadata=meta.mk_metadata(network),
                              eth_type=ether.ETH_TYPE_ARP,
                              arp_op=arp.ARP_REQUEST,
                              arp_tpa=tpa)
        instructions = [
            ofpp.OFPInstructionGotoTable(table_id=tables.TUNNEL_OUT)]
        msg = ofpp.OFPFlowMod(dp,
                              table_id=tables.ARP_PASSTHROUGH,
                              priority=1,
                              idle_timeout=5,
                              match=match,
                              instructions=instructions)
        self._send_msg(msg)
