# Copyright 2011 VMware, Inc.
# Copyright (C) 2014,2015 VA Linux Systems Japan K.K.
# Copyright (C) 2014,2015 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.

from os_ken.lib.packet import ether_types

from neutron.plugins.ml2.drivers.openvswitch.agent.common import constants
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \
    import br_dvr_process
from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \
    import ovs_bridge


class OVSTunnelBridge(ovs_bridge.OVSAgentBridge,
                      br_dvr_process.OVSDVRProcessMixin):
    """openvswitch agent tunnel bridge specific logic."""

    # Used by OVSDVRProcessMixin
    dvr_process_table_id = constants.DVR_PROCESS
    dvr_process_next_table_id = constants.PATCH_LV_TO_TUN
    of_tables = constants.TUN_BR_ALL_TABLES

    def setup_default_table(self, patch_int_ofport, arp_responder_enabled):
        (dp, ofp, ofpp) = self._get_dp()

        # Table 0 (default) will sort incoming traffic depending on in_port
        self.install_goto(dest_table_id=constants.PATCH_LV_TO_TUN,
                          priority=1,
                          in_port=patch_int_ofport)
        self.install_drop()  # default drop

        if arp_responder_enabled:
            # ARP broadcast-ed request go to the local ARP_RESPONDER table to
            # be locally resolved
            # REVISIT(yamamoto): add arp_op=arp.ARP_REQUEST matcher?
            self.install_goto(dest_table_id=constants.ARP_RESPONDER,
                              table_id=constants.PATCH_LV_TO_TUN,
                              priority=1,
                              eth_dst="ff:ff:ff:ff:ff:ff",
                              eth_type=ether_types.ETH_TYPE_ARP)

        # PATCH_LV_TO_TUN table will handle packets coming from patch_int
        # unicasts go to table UCAST_TO_TUN where remote addresses are learnt
        self.install_goto(dest_table_id=constants.UCAST_TO_TUN,
                          table_id=constants.PATCH_LV_TO_TUN,
                          eth_dst=('00:00:00:00:00:00',
                                   '01:00:00:00:00:00'))

        # Broadcasts/multicasts go to table FLOOD_TO_TUN that handles flooding
        self.install_goto(dest_table_id=constants.FLOOD_TO_TUN,
                          table_id=constants.PATCH_LV_TO_TUN,
                          eth_dst=('01:00:00:00:00:00',
                                   '01:00:00:00:00:00'))

        # Tables [tunnel_type]_TUN_TO_LV will set lvid depending on tun_id
        # for each tunnel type, and resubmit to table LEARN_FROM_TUN where
        # remote mac addresses will be learnt
        for tunnel_type in constants.TUNNEL_NETWORK_TYPES:
            self.install_drop(table_id=constants.TUN_TABLE[tunnel_type])

        # LEARN_FROM_TUN table will have a single flow using a learn action to
        # dynamically set-up flows in UCAST_TO_TUN corresponding to remote mac
        # addresses (assumes that lvid has already been set by a previous flow)
        # Once remote mac addresses are learnt, output packet to patch_int
        flow_specs = [
            ofpp.NXFlowSpecMatch(src=('vlan_tci', 0),
                                 dst=('vlan_tci', 0),
                                 n_bits=12),
            ofpp.NXFlowSpecMatch(src=('eth_src', 0),
                                 dst=('eth_dst', 0),
                                 n_bits=48),
            ofpp.NXFlowSpecLoad(src=0,
                                dst=('vlan_tci', 0),
                                n_bits=16),
            ofpp.NXFlowSpecLoad(src=('tunnel_id', 0),
                                dst=('tunnel_id', 0),
                                n_bits=64),
            ofpp.NXFlowSpecOutput(src=('in_port', 0),
                                  dst='',
                                  n_bits=32),
        ]
        actions = [
            ofpp.NXActionLearn(table_id=constants.UCAST_TO_TUN,
                               cookie=self.default_cookie,
                               priority=1,
                               hard_timeout=300,
                               specs=flow_specs),
            ofpp.OFPActionOutput(patch_int_ofport, 0),
        ]
        self.install_apply_actions(table_id=constants.LEARN_FROM_TUN,
                                   priority=1,
                                   actions=actions)

        # Egress unicast will be handled in table UCAST_TO_TUN, where remote
        # mac addresses will be learned. For now, just add a default flow that
        # will resubmit unknown unicasts to table FLOOD_TO_TUN to treat them
        # as broadcasts/multicasts
        self.install_goto(dest_table_id=constants.FLOOD_TO_TUN,
                          table_id=constants.UCAST_TO_TUN)

        if arp_responder_enabled:
            # If none of the ARP entries correspond to the requested IP, the
            # broadcast-ed packet is resubmitted to the flooding table
            self.install_goto(dest_table_id=constants.FLOOD_TO_TUN,
                              table_id=constants.ARP_RESPONDER)

        # FLOOD_TO_TUN will handle flooding in tunnels based on lvid,
        # for now, add a default drop action
        self.install_drop(table_id=constants.FLOOD_TO_TUN)

    @staticmethod
    def _local_vlan_match(_ofp, ofpp, tun_id):
        return ofpp.OFPMatch(tunnel_id=tun_id)

    def provision_local_vlan(self, network_type, lvid, segmentation_id,
                             distributed=False):
        (_dp, ofp, ofpp) = self._get_dp()
        match = self._local_vlan_match(ofp, ofpp, segmentation_id)
        table_id = constants.TUN_TABLE[network_type]
        if distributed:
            dest_table_id = constants.DVR_NOT_LEARN
        else:
            dest_table_id = constants.LEARN_FROM_TUN
        actions = [
            ofpp.OFPActionPushVlan(),
            ofpp.OFPActionSetField(vlan_vid=lvid | ofp.OFPVID_PRESENT),
        ]
        instructions = [
            ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions),
            ofpp.OFPInstructionGotoTable(table_id=dest_table_id)]
        self.install_instructions(table_id=table_id,
                                  priority=1,
                                  match=match,
                                  instructions=instructions)

    def reclaim_local_vlan(self, network_type, segmentation_id):
        (_dp, ofp, ofpp) = self._get_dp()
        match = self._local_vlan_match(ofp, ofpp, segmentation_id)
        table_id = constants.TUN_TABLE[network_type]
        self.uninstall_flows(table_id=table_id, match=match)

    @staticmethod
    def _flood_to_tun_match(ofp, ofpp, vlan):
        return ofpp.OFPMatch(vlan_vid=vlan | ofp.OFPVID_PRESENT)

    def install_flood_to_tun(self, vlan, tun_id, ports):
        (_dp, ofp, ofpp) = self._get_dp()
        match = self._flood_to_tun_match(ofp, ofpp, vlan)
        actions = [ofpp.OFPActionPopVlan(),
                   ofpp.OFPActionSetField(tunnel_id=tun_id)]
        for port in ports:
            actions.append(ofpp.OFPActionOutput(port, 0))
        self.install_apply_actions(table_id=constants.FLOOD_TO_TUN,
                                   priority=1,
                                   match=match,
                                   actions=actions)

    def delete_flood_to_tun(self, vlan):
        (_dp, ofp, ofpp) = self._get_dp()
        match = self._flood_to_tun_match(ofp, ofpp, vlan)
        self.uninstall_flows(table_id=constants.FLOOD_TO_TUN, match=match)

    @staticmethod
    def _unicast_to_tun_match(ofp, ofpp, vlan, mac):
        return ofpp.OFPMatch(vlan_vid=vlan | ofp.OFPVID_PRESENT, eth_dst=mac)

    def install_unicast_to_tun(self, vlan, tun_id, port, mac):
        (_dp, ofp, ofpp) = self._get_dp()
        match = self._unicast_to_tun_match(ofp, ofpp, vlan, mac)
        actions = [ofpp.OFPActionPopVlan(),
                   ofpp.OFPActionSetField(tunnel_id=tun_id),
                   ofpp.OFPActionOutput(port, 0)]
        self.install_apply_actions(table_id=constants.UCAST_TO_TUN,
                                   priority=2,
                                   match=match,
                                   actions=actions)

    def delete_unicast_to_tun(self, vlan, mac):
        (_dp, ofp, ofpp) = self._get_dp()
        if mac is None:
            match = ofpp.OFPMatch(vlan_vid=vlan | ofp.OFPVID_PRESENT)
        else:
            match = self._unicast_to_tun_match(ofp, ofpp, vlan, mac)
        self.uninstall_flows(table_id=constants.UCAST_TO_TUN, match=match)

    @staticmethod
    def _arp_responder_match(ofp, ofpp, vlan, ip):
        # REVISIT(yamamoto): add arp_op=arp.ARP_REQUEST matcher?
        return ofpp.OFPMatch(vlan_vid=vlan | ofp.OFPVID_PRESENT,
                             eth_type=ether_types.ETH_TYPE_ARP,
                             arp_tpa=ip)

    def delete_arp_responder(self, vlan, ip):
        (_dp, ofp, ofpp) = self._get_dp()
        if ip is None:
            # REVISIT(yamamoto): add arp_op=arp.ARP_REQUEST matcher?
            match = ofpp.OFPMatch(vlan_vid=vlan | ofp.OFPVID_PRESENT,
                                  eth_type=ether_types.ETH_TYPE_ARP)
        else:
            match = self._arp_responder_match(ofp, ofpp, vlan, ip)
        self.uninstall_flows(table_id=constants.ARP_RESPONDER,
                             match=match)

    def setup_tunnel_port(self, network_type, port):
        self.install_goto(dest_table_id=constants.TUN_TABLE[network_type],
                          priority=1,
                          in_port=port)

    def cleanup_tunnel_port(self, port):
        self.uninstall_flows(in_port=port)

    def add_dvr_mac_tun(self, mac, port):
        self.install_output(table_id=constants.DVR_NOT_LEARN,
                            priority=1,
                            eth_src=mac,
                            port=port)

    def remove_dvr_mac_tun(self, mac):
        # REVISIT(yamamoto): match in_port as well?
        self.uninstall_flows(table_id=constants.DVR_NOT_LEARN,
                             eth_src=mac)
