# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
# 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 itertools

import netaddr
from oslo_config import cfg
from oslo_log import log

from neutron.agent.common import ovs_lib
from neutron.agent import firewall
from neutron.common import constants

from networking_vsphere.common import constants as ovsvapp_const

LOG = log.getLogger(__name__)

PROTOCOLS = {constants.PROTO_NAME_TCP: constants.PROTO_NAME_TCP,
             constants.PROTO_NUM_TCP: constants.PROTO_NAME_TCP,
             constants.PROTO_NAME_UDP: constants.PROTO_NAME_UDP,
             constants.PROTO_NUM_UDP: constants.PROTO_NAME_UDP,
             constants.PROTO_NAME_ICMP: constants.PROTO_NAME_ICMP,
             constants.PROTO_NUM_ICMP: constants.PROTO_NAME_ICMP,
             constants.PROTO_NAME_ICMP_V6: constants.PROTO_NAME_ICMP_V6,
             constants.PROTO_NUM_ICMP_V6: constants.PROTO_NAME_ICMP_V6}

ETHERTYPE = {constants.IPv4: "ip",
             constants.IPv6: "ipv6"}

INGRESS_DIRECTION = 'ingress'
EGRESS_DIRECTION = 'egress'

sg_conf = cfg.CONF.SECURITYGROUP

PORT_KEYS = ['security_group_source_groups',
             'mac_address',
             'network_id',
             'id',
             'security_groups',
             'lvid']


class OVSFirewallDriver(firewall.FirewallDriver):
    """Driver which enforces security groups through OVS flows."""

    def __init__(self):
        self.filtered_ports = {}
        if sg_conf.security_bridge_mapping is None:
            LOG.debug("Security_bridge_mapping not configured.")
            return
        secbr_list = (sg_conf.security_bridge_mapping).split(':')
        secbr_name = secbr_list[0]
        secbr_phyname = secbr_list[1]
        self.sg_br = ovs_lib.OVSBridge(secbr_name)
        self.phy_ofport = self.sg_br.get_port_ofport(secbr_phyname)
        self.patch_ofport = self.sg_br.get_port_ofport(
            ovsvapp_const.SEC_TO_INT_PATCH)
        self._defer_apply = False
        if not cfg.CONF.OVSVAPP.agent_maintenance:
            self.setup_base_flows()

    @property
    def ports(self):
        return self.filtered_ports

    def _get_compact_port(self, port):
        if 'lvid' not in port:
            if port['id'] in self.filtered_ports:
                old_port = self.filtered_ports[port['id']]
                port['lvid'] = old_port['lvid']
        new_port = {}
        new_port['device'] = port['id']
        for key in PORT_KEYS:
            if key in port:
                new_port[key] = port[key]
        return new_port

    def _add_ovs_flow(self, sg_br, pri, table_id, action,
                      protocol=None, dl_dest=None,
                      tcp_flag=None, icmp_req_type=None):
        """Helper method for adding OVS flows.

        Method which will help add an openflow rule with the given
        priority and action in the specified table.
        """
        if protocol:
            sg_br.add_flow(table=table_id, priority=pri,
                           proto=protocol, actions=action)
        elif dl_dest:
            sg_br.add_flow(table=table_id, priority=pri,
                           dl_dst=dl_dest, actions=action)
        elif tcp_flag:
            sg_br.add_flow(table=table_id, priority=pri,
                           proto=constants.PROTO_NAME_TCP,
                           tcp_flags=tcp_flag, actions=action)
        elif icmp_req_type:
            sg_br.add_flow(table=table_id, priority=pri,
                           proto=constants.PROTO_NAME_ICMP,
                           icmp_type=icmp_req_type,
                           actions=action)
        else:
            sg_br.add_flow(table=table_id, priority=pri, actions=action)

    def _add_icmp_learn_flow(self, sec_br, reqType, resType,
                             pri=ovsvapp_const.SG_TP_PRI):
        sec_br.add_flow(priority=pri,
                        table=ovsvapp_const.SG_ICMP_TABLE_ID,
                        proto=constants.PROTO_NAME_ICMP,
                        icmp_type=reqType,
                        actions="learn(%s)" %
                        self._get_icmp_learn_flow(resType))

    def _get_icmp_learn_flow(self, resType):
        if resType is ovsvapp_const.ICMP_DEST_UNREACH:
            ip_str = ""
        else:
            ip_str = "NXM_OF_IP_SRC[]=NXM_OF_IP_DST[],"
        return ("table=%s,"
                "priority=%s,"
                "idle_timeout=30,"
                "dl_type=0x0800,"
                "NXM_OF_VLAN_TCI[0..11],"
                "nw_proto=%s,"
                "icmp_type=%s,"
                "%s"
                "NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],"
                "output:NXM_OF_IN_PORT[]" %
                (ovsvapp_const.SG_LEARN_TABLE_ID,
                 ovsvapp_const.SG_TP_PRI,
                 constants.PROTO_NUM_ICMP,
                 resType, ip_str))

    def _setup_icmp_learn_flows(self, sec_br):
        # ICMP Learned flows
        self._add_icmp_learn_flow(sec_br, ovsvapp_const.ICMP_ECHO_REQ,
                                  ovsvapp_const.ICMP_ECHO_REP)
        self._add_icmp_learn_flow(sec_br, ovsvapp_const.ICMP_TS_REQ,
                                  ovsvapp_const.ICMP_TS_REP)
        self._add_icmp_learn_flow(sec_br, ovsvapp_const.ICMP_INFO_REQ,
                                  ovsvapp_const.ICMP_INFO_REP)
        self._add_icmp_learn_flow(sec_br, ovsvapp_const.ICMP_AM_REQ,
                                  ovsvapp_const.ICMP_AM_REP)

    def _setup_learning_flows(self, sec_br):
        """Helper method for adding learning flows.

        Method which will help setup the base learning flows at
        the start of the agent.
        These flows are populated in specific tables for
        TCP/UDP/ICMP.
        """
        # First we chain the tables.
        self._add_ovs_flow(sec_br, ovsvapp_const.SG_DEFAULT_PRI,
                           ovsvapp_const.SG_ICMP_TABLE_ID, "drop")
        # If DMAC is bcast or mcast, don't learn.
        self._add_ovs_flow(sec_br, ovsvapp_const.SG_DROP_HIGH_PRI,
                           ovsvapp_const.SG_IP_TABLE_ID, "drop",
                           dl_dest="01:00:00:00:00:00/01:00:00:00:00:00")

        # Now we add learning flows one for TCP and another for UDP.
        learned_tcp_flow = ("table=%s,"
                            "priority=%s,"
                            "fin_idle_timeout=1,"
                            "idle_timeout=7200,"
                            "nw_proto=%s,"
                            "NXM_OF_ETH_SRC[]=NXM_OF_ETH_DST[],"
                            "NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],"
                            "dl_type=0x0800,"
                            "NXM_OF_VLAN_TCI[0..11],"
                            "NXM_OF_IP_SRC[]=NXM_OF_IP_DST[],"
                            "NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],"
                            "NXM_OF_TCP_SRC[]=NXM_OF_TCP_DST[],"
                            "NXM_OF_TCP_DST[]=NXM_OF_TCP_SRC[],"
                            "output:NXM_OF_IN_PORT[]" %
                            (ovsvapp_const.SG_LEARN_TABLE_ID,
                             ovsvapp_const.SG_TP_PRI,
                             constants.PROTO_NUM_TCP))
        self._add_ovs_flow(sec_br, ovsvapp_const.SG_TP_PRI,
                           ovsvapp_const.SG_TCP_TABLE_ID,
                           "learn(%s)" % learned_tcp_flow,
                           protocol=constants.PROTO_NAME_TCP)

        learned_udp_flow = ("table=%s,"
                            "priority=%s,"
                            "idle_timeout=300,"
                            "nw_proto=%s,"
                            "NXM_OF_ETH_SRC[]=NXM_OF_ETH_DST[],"
                            "NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],"
                            "dl_type=0x0800,"
                            "NXM_OF_VLAN_TCI[0..11],"
                            "NXM_OF_IP_SRC[]=NXM_OF_IP_DST[],"
                            "NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],"
                            "NXM_OF_UDP_SRC[]=NXM_OF_UDP_DST[],"
                            "NXM_OF_UDP_DST[]=NXM_OF_UDP_SRC[],"
                            "output:NXM_OF_IN_PORT[]" %
                            (ovsvapp_const.SG_LEARN_TABLE_ID,
                             ovsvapp_const.SG_TP_PRI,
                             constants.PROTO_NUM_UDP))
        self._add_ovs_flow(sec_br, ovsvapp_const.SG_TP_PRI,
                           ovsvapp_const.SG_UDP_TABLE_ID,
                           "learn(%s)" % learned_udp_flow,
                           protocol=constants.PROTO_NAME_UDP)
        # Now setup the ICMP learn flows.
        self._setup_icmp_learn_flows(sec_br)

    def setup_base_flows(self):
        """Method for configuring the default flows in OVS bridge.

        Method which will help setup the base flows at the start
        of the agent.
        """
        try:
            sec_br = self.sg_br
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_DEFAULT_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "resubmit(,%s)" %
                               ovsvapp_const.SG_LEARN_TABLE_ID)
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_DROPALL_PRI,
                               ovsvapp_const.SG_LEARN_TABLE_ID, "drop")
            # Allow all ARP, parity with iptables.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_RULES_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "normal", protocol="arp")
            # Allow all RARP, parity with iptables.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_RULES_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "normal", protocol="rarp")
            # Rule to allow VMs to send DHCP requests (udp).
            sec_br.add_flow(priority=ovsvapp_const.SG_RULES_PRI,
                            table=ovsvapp_const.SG_DEFAULT_TABLE_ID,
                            proto="udp", tp_src="68", tp_dst="67",
                            actions="normal")
            # Always allow ICMP DestUnreach.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_TP_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "normal", icmp_req_type=ovsvapp_const.
                               ICMP_DEST_UNREACH)

            # Always allow ICMP TTL Exceeded.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_TP_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID, "normal",
                               icmp_req_type=ovsvapp_const.
                               ICMP_TIME_EXCEEDED)

            self._setup_learning_flows(sec_br)

            # Always resubmit FIN pkts to learn table.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_TCP_FLAG_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "resubmit(,%s),normal" %
                               ovsvapp_const.SG_LEARN_TABLE_ID,
                               tcp_flag='+fin')
            # Always resubmit RST pkts to learn table.
            self._add_ovs_flow(sec_br, ovsvapp_const.SG_TCP_FLAG_PRI,
                               ovsvapp_const.SG_DEFAULT_TABLE_ID,
                               "resubmit(,%s),normal" %
                               ovsvapp_const.SG_LEARN_TABLE_ID,
                               tcp_flag='+rst')
        except Exception:
            LOG.exception(_("Unable to add base flows."))

    def add_ports_to_filter(self, ports):
        for port in ports:
            LOG.debug("OVSF Adding port: %s to filter.", port)
            self.filtered_ports[port['id']] = self._get_compact_port(port)

    def _get_port_vlan(self, port_id):
        if port_id:
            port = self.filtered_ports.get(port_id)
            LOG.debug("Filtered port: %s.", port)
            if port:
                return port['lvid']

    def _setup_aap_flows(self, sec_br, port):
        """Method to help setup rules for allowed address pairs."""
        vlan = self._get_port_vlan(port['id'])
        if not vlan:
            LOG.error(_("Missing VLAN information for port: %s."), port['id'])
            return
        if isinstance(port.get('allowed_address_pairs'), list):
            for addr_pair in port['allowed_address_pairs']:
                if netaddr.IPNetwork(addr_pair["ip_address"]).version == 4:
                    ap_proto = "ip"
                else:
                    ap_proto = "ipv6"
                sec_br.add_flow(priority=ovsvapp_const.SG_RULES_PRI,
                                table=ovsvapp_const.SG_DEFAULT_TABLE_ID,
                                cookie=self.get_cookie(port['id']),
                                dl_dst=port["mac_address"],
                                in_port=self.patch_ofport,
                                dl_src=addr_pair["mac_address"],
                                dl_vlan=vlan,
                                proto=ap_proto,
                                nw_src=addr_pair["ip_address"],
                                actions="resubmit(,%s),output:%s" %
                                (ovsvapp_const.SG_IP_TABLE_ID,
                                 self.phy_ofport))

    def _get_net_prefix_len(self, ip_prefix):
        if ip_prefix:
            return netaddr.IPNetwork(ip_prefix).prefixlen
        else:
            return 0

    def _get_protocol(self, ethertype, proto):
        if ethertype == constants.IPv6 and (
            proto == constants.PROTO_NAME_ICMP):
            return ['icmp6']
        elif proto is not None:
            protocol = PROTOCOLS.get(proto)
            if protocol is None:
                return [ETHERTYPE.get(ethertype), proto]
            else:
                return [protocol]
        elif ethertype == constants.IPv4:
            return ['ip']
        elif ethertype == constants.IPv6:
            return ['ipv6']

    def _add_flow_with_range(self, sec_br, flow,
                             pr_min=None, pr_max=None,
                             spr_min=None, spr_max=None):
        if pr_min is None and pr_max is None:
            pr_min = -1
            pr_max = -1
        if spr_min is None and spr_max is None:
            spr_min = -1
            spr_max = -1

        for dport, sport in itertools.product(xrange(pr_min, pr_max + 1),
                                              xrange(spr_min, spr_max + 1)):
            if dport >= 0:
                flow["tp_dst"] = dport
            if sport >= 0:
                flow["tp_src"] = sport
            sec_br.add_flow(**flow)

    def _add_flows(self, sec_br, port, rules=None):
        egress_action = 'normal'
        ingress_action = 'output:%s' % self.phy_ofport

        if not rules:
            rules = port["security_group_rules"]

        vlan = self._get_port_vlan(port['id'])
        if not vlan:
            LOG.error(_('Missing VLAN for port: %s.'), port['id'])
            return
        for rule in rules:
            direction = rule.get('direction')
            proto = rule.get('protocol')
            pr_min = rule.get('port_range_min')
            pr_max = rule.get('port_range_max')
            spr_min = rule.get('source_port_range_min')
            spr_max = rule.get('source_port_range_max')
            ethertype = rule.get('ethertype')
            src_ip_prefix = rule.get('source_ip_prefix')
            dest_ip_prefix = rule.get('dest_ip_prefix')
            flow = dict(priority=ovsvapp_const.SG_RULES_PRI)
            flow["table"] = ovsvapp_const.SG_DEFAULT_TABLE_ID
            # Using port id as cookie.
            flow["cookie"] = self.get_cookie(port['id'])
            flow["dl_vlan"] = vlan
            # Fill the src and dest IPs match params.
            src_ip_prefixlen = self._get_net_prefix_len(src_ip_prefix)
            if src_ip_prefixlen > 0:
                flow["nw_src"] = src_ip_prefix
            dest_ip_prefixlen = self._get_net_prefix_len(dest_ip_prefix)
            if dest_ip_prefixlen > 0:
                flow["nw_dst"] = dest_ip_prefix
            # Fill the protocol related  match params.
            protocols = self._get_protocol(ethertype, proto)
            protocol = protocols[0]
            flow["proto"] = protocol
            if len(protocols) > 1:
                flow["nw_proto"] = protocols[1]
            # set source and destination params and action for the flow.
            if direction == INGRESS_DIRECTION:
                flow["dl_dst"] = port["mac_address"]
                flow["in_port"] = self.patch_ofport
                action = ingress_action
            elif direction == EGRESS_DIRECTION:
                flow["dl_src"] = port["mac_address"]
                flow["in_port"] = self.phy_ofport
                action = egress_action
            tcp_udp = set([constants.PROTO_NAME_TCP,
                           constants.PROTO_NAME_UDP])
            table_id = None
            if protocol in tcp_udp:
                flow["priority"] = ovsvapp_const.SG_TP_PRI
                if protocol == constants.PROTO_NAME_TCP:
                    table_id = ovsvapp_const.SG_TCP_TABLE_ID
                else:
                    table_id = ovsvapp_const.SG_UDP_TABLE_ID
                flow["actions"] = ("resubmit(,%s),%s" % (table_id, action))
                self._add_flow_with_range(sec_br, flow, pr_min, pr_max,
                                          spr_min, spr_max)
                # Since we added the required flows in the above method
                # we just proceed to the next sg rule.
                continue
            elif protocol == constants.PROTO_NAME_ICMP:
                flow["priority"] = ovsvapp_const.SG_TP_PRI
                if pr_min is not None:
                    flow["icmp_type"] = pr_min
                if pr_max is not None:
                    flow["icmp_code"] = pr_max
                table_id = ovsvapp_const.SG_ICMP_TABLE_ID
            else:
                table_id = ovsvapp_const.SG_IP_TABLE_ID

            flow["actions"] = ("resubmit(,%s),%s" % (table_id, action))
            sec_br.add_flow(**flow)
            LOG.debug("OVSF adding flow: %s", flow)

    def prepare_port_filter(self, port):
        """Method to add OVS rules for a newly created VM port."""
        LOG.debug("OVSF Preparing port %s filter.", port['id'])
        try:
            with self.sg_br.deferred(full_ordered=True, order=(
                'del', 'mod', 'add')) as deferred_br:
                self._setup_aap_flows(deferred_br, port)
                self._add_flows(deferred_br, port)
            self.filtered_ports[port['id']] = self._get_compact_port(port)
        except Exception:
            LOG.exception(_("Unable to add flows for %s."), port['id'])

    def _remove_flows(self, sec_br, port_id):
        """Remove all flows for a port."""
        LOG.debug("OVSF Removing flows start for port: %s.", port_id)
        try:
            sec_br.delete_flows(cookie="%s/-1" %
                                self.get_cookie(port_id))
            port = self.filtered_ports.get(port_id)
            vlan = self._get_port_vlan(port_id)
            if 'mac_address' not in port or not vlan:
                LOG.debug("Invalid mac address or vlan for port "
                          "%s. Returning from _remove_flows.", port_id)
                return
            sec_br.delete_flows(table=ovsvapp_const.SG_LEARN_TABLE_ID,
                                dl_src=port['mac_address'],
                                vlan_tci="0x%04x/0x0fff" % vlan)
            sec_br.delete_flows(table=ovsvapp_const.SG_LEARN_TABLE_ID,
                                dl_dst=port['mac_address'],
                                vlan_tci="0x%04x/0x0fff" % vlan)
            sec_br.delete_flows(table=ovsvapp_const.SG_DEFAULT_TABLE_ID,
                                dl_src=port['mac_address'],
                                vlan_tci="0x%04x/0x0fff" % vlan)
            sec_br.delete_flows(table=ovsvapp_const.SG_DEFAULT_TABLE_ID,
                                dl_dst=port['mac_address'],
                                vlan_tci="0x%04x/0x0fff" % vlan)
        except Exception:
            LOG.exception(_("Unable to remove flows %s."), port['id'])

    def clean_port_filters(self, ports, remove_port=False):
        """Method to remove OVS rules for an existing VM port."""
        LOG.debug("OVSF Cleaning filters for  %s ports.", len(ports))
        if not ports:
            return
        with self.sg_br.deferred() as deferred_sec_br:
            for port_id in ports:
                try:
                    if not self.filtered_ports.get(port_id):
                        LOG.debug("Attempted to remove port filter "
                                  "which is not in filtered %s.", port_id)
                        continue
                    self._remove_flows(deferred_sec_br, port_id)
                    if remove_port:
                        self.filtered_ports.pop(port_id, None)
                except Exception:
                    LOG.exception(_("Unable to delete flows for %s."), port_id)

    def update_port_filter(self, port):
        """Method to update OVS rules for an existing VM port."""
        LOG.debug("OVSF Updating port: %s filter.", port['id'])
        if port['id'] not in self.filtered_ports:
            LOG.warn(_("Attempted to update port filter which is not "
                     "filtered %s."), port['id'])
            return
        try:
            with self.sg_br.deferred(full_ordered=True, order=(
                'del', 'mod', 'add')) as deferred_br:
                self._remove_flows(deferred_br, port['id'])
                self._setup_aap_flows(deferred_br, port)
                self._add_flows(deferred_br, port)
            self.filtered_ports[port['id']] = self._get_compact_port(port)
        except Exception:
            LOG.exception(_("Unable to update flows for %s."), port['id'])

    def filter_defer_apply_on(self):
        if not self._defer_apply:
            self._defer_apply = True

    def filter_defer_apply_off(self):
        if self._defer_apply:
            self._defer_apply = False

    def get_cookie(self, port_id):
        return ("0x%x" % (hash(port_id) & 0xffffffffffffffff))
