# Copyright 2015
# 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

from neutron_lib import constants as lib_const
from neutron_lib.plugins.ml2 import ovs_constants as ovs_consts
from oslo_log import log as logging
from oslo_utils import netutils

from neutron.agent import firewall

from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2\
    import driver_base
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
    openvswitch_firewall import constants as fwaas_ovs_consts
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
    openvswitch_firewall import exceptions
from neutron_fwaas.services.firewall.service_drivers.agents.drivers.linux.l2.\
    openvswitch_firewall import rules

LOG = logging.getLogger(__name__)

ACTION_ALLOW = 'allow'


# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver.
def _replace_register(flow_params, register_number, register_value):
    """Replace value from flows to given register number

    'register_value' key in dictionary will be replaced by register number
    given by 'register_number'

    :param flow_params: Dictionary containing defined flows
    :param register_number: The number of register where value will be stored
    :param register_value: Key to be replaced by register number

    """
    try:
        reg_port = flow_params[register_value]
        del flow_params[register_value]
        flow_params[f'reg{register_number:d}'] = reg_port
    except KeyError:
        pass


# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs only in constants REG_PORT/REG_NET.
def create_reg_numbers(flow_params):
    """Replace reg_(port|net) values with defined register numbers"""
    _replace_register(flow_params, fwaas_ovs_consts.REG_PORT, 'reg_port')
    _replace_register(flow_params, fwaas_ovs_consts.REG_NET, 'reg_net')


class FirewallGroup:
    def __init__(self, id_):
        self.id = id_
        self.ingress_rules = []
        self.egress_rules = []
        self.members = {}
        self.ports = set()

    def update_rules(self, ingress_rules, egress_rules):
        """Update firewall group with ingress/egress rules.

        If a rule has a protocol field, it is normalized to a number
        here in order to ease later processing.
        """
        def _translate_protocol_to_number(rule):
            protocol = rule.get('protocol')
            if protocol is not None:
                if protocol.isdigit():
                    rule['protocol'] = int(protocol)
                elif (rule.get('ethertype') == lib_const.IPv6 and
                        protocol == lib_const.PROTO_NAME_ICMP):
                    rule['protocol'] = lib_const.PROTO_NUM_IPV6_ICMP
                else:
                    rule['protocol'] = lib_const.IP_PROTOCOL_MAP.get(
                        protocol, protocol)
            return rule

        self.ingress_rules = [_translate_protocol_to_number(ir)
                              for ir in ingress_rules]
        self.egress_rules = [_translate_protocol_to_number(er)
                             for er in egress_rules]

    def get_ethertype_filtered_addresses(self, ethertype,
                                         exclude_addresses=None):
        exclude_addresses = set(exclude_addresses if exclude_addresses else [])
        group_addresses = set(self.members.get(ethertype, []))
        return list(group_addresses - exclude_addresses)


# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs only in firewall groups list field name
class OFPort:
    def __init__(self, port_dict, ovs_port, vlan_tag):
        self.id = port_dict['device']
        self.vlan_tag = vlan_tag
        self.mac = ovs_port.vif_mac
        self.lla_address = str(netutils.get_ipv6_addr_by_EUI64(
            lib_const.IPv6_LLA_PREFIX, self.mac))
        self.ofport = ovs_port.ofport
        self.fw_group = None
        self.fixed_ips = port_dict.get('fixed_ips', [])
        self.neutron_port_dict = port_dict.copy()
        self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict, version=4)
        self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict, version=6)

    @staticmethod
    def _get_allowed_pairs(port_dict, version):
        aap_dict = port_dict.get('allowed_address_pairs', set())
        return {(aap['mac_address'], aap['ip_address']) for aap in aap_dict
                if netaddr.IPNetwork(aap['ip_address']).version == version}

    @property
    def all_allowed_macs(self):
        macs = {item[0] for item in self.allowed_pairs_v4.union(
            self.allowed_pairs_v6)}
        macs.add(self.mac)
        return macs

    @property
    def ipv4_addresses(self):
        return [ip_addr for ip_addr in
                [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips]
                if netaddr.IPAddress(ip_addr).version == 4]

    @property
    def ipv6_addresses(self):
        return [ip_addr for ip_addr in
                [fixed_ip['ip_address'] for fixed_ip in self.fixed_ips]
                if netaddr.IPAddress(ip_addr).version == 6]

    def update(self, port_dict):
        self.allowed_pairs_v4 = self._get_allowed_pairs(port_dict,
                                                        version=4)
        self.allowed_pairs_v6 = self._get_allowed_pairs(port_dict,
                                                        version=6)
        # Neighbour discovery uses LLA
        self.allowed_pairs_v6.add((self.mac, self.lla_address))
        self.fixed_ips = port_dict.get('fixed_ips', [])
        self.neutron_port_dict = port_dict.copy()


# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# differs in methods name [s/sg/fwg] and update_rules method.
class FWGPortMap:
    def __init__(self):
        self.ports = {}
        self.fw_groups = {}
        # Maps port_id to ofport number
        self.unfiltered = {}

    def get_fwg(self, fwg_id):
        return self.fw_groups.get(fwg_id, None)

    def get_or_create_fwg(self, fwg_id):
        fw_group = self.get_fwg(fwg_id)
        if not fw_group:
            fw_group = FirewallGroup(fwg_id)
            self.fw_groups[fwg_id] = fw_group
        return fw_group

    def delete_fwg(self, fwg_id):
        del self.fw_groups[fwg_id]

    # XXX NOTE(ivasilevskaya) couldn't find any logical definition why
    # firewall_group should come as 3rd argument instead of adding fwg_id
    # to port_dict. Removed in favor of SG api
    def create_port(self, port, port_dict):
        self.ports[port.id] = port
        self.update_port(port, port_dict)

    # XXX NOTE(ivasilevskaya) couldn't find any logical definition why
    # firewall_group should come as 3rd argument instead of adding fwg_id
    # to port_dict. Removed in favor of SG api
    def update_port(self, port, port_dict):
        for fw_group in self.fw_groups.values():
            fw_group.ports.discard(port)

        fw_group = self.get_or_create_fwg(port_dict['firewall_group'])
        port.fw_group = fw_group
        fw_group.ports.add(port)
        port.update(port_dict)

    def remove_port(self, port):
        if port.fw_group:
            port.fw_group.ports.discard(port)
        del self.ports[port.id]

    def update_rules(self, fwg_id, ingress_rules, egress_rules):
        fw_group = self.get_or_create_fwg(fwg_id)
        fw_group.update_rules(ingress_rules, egress_rules)

    def update_members(self, fwg_id, members):
        fw_group = self.get_or_create_fwg(fwg_id)
        fw_group.members = members


# NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver that
# doesn't have a conjunction manager because no remote_group_id concept is
# applicable to firewall groups
class OVSFirewallDriver(driver_base.FirewallL2DriverBase):
    REQUIRED_PROTOCOLS = [
        ovs_consts.OPENFLOW10,
        ovs_consts.OPENFLOW11,
        ovs_consts.OPENFLOW12,
        ovs_consts.OPENFLOW13,
        ovs_consts.OPENFLOW14,
    ]

    provides_arp_spoofing_protection = True

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver.
    # This driver won't have any conj_manager logic because there is no concept
    # of remote_group_id for firewall groups (that I know of at least)
    def __init__(self, agent_api, sg_with_ovs=False):
        """Initialize object"""

        integration_bridge = agent_api.request_int_br()
        self.int_br = self.initialize_bridge(integration_bridge)
        self.fwg_port_map = FWGPortMap()
        self.fwg_to_delete = set()
        self._deferred = False
        self.sg_with_ovs = sg_with_ovs
        self._drop_all_unmatched_flows()
        self._initialize_third_party_tables()

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def _accept_flow(self, **flow):
        for f in rules.create_accept_flows(flow, self.sg_with_ovs):
            self._add_flow(**f)

    def _drop_flow(self, **flow):
        for f in rules.create_drop_flows(flow):
            self._add_flow(**f)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def _add_flow(self, **kwargs):
        dl_type = kwargs.get('dl_type')
        create_reg_numbers(kwargs)
        if isinstance(dl_type, int):
            kwargs['dl_type'] = f"0x{dl_type:04x}"
        if self._deferred:
            self.int_br.add_flow(**kwargs)
        else:
            self.int_br.br.add_flow(**kwargs)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def _delete_flows(self, **kwargs):
        create_reg_numbers(kwargs)
        if self._deferred:
            self.int_br.delete_flows(**kwargs)
        else:
            self.int_br.br.delete_flows(**kwargs)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def _strict_delete_flow(self, **kwargs):
        """Delete given flow right away even if bridge is deferred.

        Delete command will use strict delete.
        """
        create_reg_numbers(kwargs)
        self.int_br.br.delete_flows(strict=True, **kwargs)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    @staticmethod
    def initialize_bridge(int_br):
        int_br.add_protocols(*OVSFirewallDriver.REQUIRED_PROTOCOLS)
        return int_br.deferred(full_ordered=True)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
    # differs in constants
    def _drop_all_unmatched_flows(self):
        for table in fwaas_ovs_consts.OVS_FIREWALL_TABLES:
            if (table == fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE and
                    self.sg_with_ovs):
                continue
            self.int_br.br.add_flow(table=table, priority=0, actions='drop')

    def _initialize_third_party_tables(self):
        self.int_br.br.add_flow(
            table=ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE,
            priority=1,
            actions='normal')
        for table in (ovs_consts.ACCEPTED_INGRESS_TRAFFIC_TABLE,
                      ovs_consts.DROPPED_TRAFFIC_TABLE):
            self.int_br.br.add_flow(
                table=table, priority=0, actions='drop')

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def get_ovs_port(self, port_id):
        ovs_port = self.int_br.br.get_vif_port_by_id(port_id)
        if not ovs_port:
            raise exceptions.OVSFWaaSPortNotFound(port_id=port_id)
        return ovs_port

    def _get_port_vlan_tag(self, port):
        vlan_tag = port.get('lvlan', None)
        if not vlan_tag:
            raise exceptions.OVSFWaaSTagNotFound(port_id=port['device'])
        return vlan_tag

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def get_ofport(self, port):
        port_id = port['device']
        return self.fwg_port_map.ports.get(port_id)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
    # self.sg_port_map -> self.fwg_port_map
    def get_or_create_ofport(self, port):
        """Get ofport specified by port['device'], checking and reflecting
        ofport changes.
        If ofport is nonexistent, create and return one.
        """
        port_id = port['device']
        ovs_port = self.get_ovs_port(port_id)
        try:
            of_port = self.fwg_port_map.ports[port_id]
        except KeyError:
            port_vlan_id = self._get_port_vlan_tag(port)
            of_port = OFPort(port, ovs_port, port_vlan_id)
            self.fwg_port_map.create_port(of_port, port)
        else:
            if of_port.ofport != ovs_port.ofport:
                self.fwg_port_map.remove_port(of_port)
                of_port = OFPort(port, ovs_port, of_port.vlan_tag)
            self.fwg_port_map.update_port(of_port, port)

        return of_port

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def is_port_managed(self, port):
        return port['device'] in self.fwg_port_map.ports

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def prepare_port_filter(self, port):
        # NOTE(annp): port no security should be handled by security group in
        # co-existence mode, otherwise(standalone mode) fwg will handle it.
        if not firewall.port_sec_enabled(port) and not self.sg_with_ovs:
            self._initialize_egress_no_port_security(port)
            return
        old_of_port = self.get_ofport(port)
        # Make sure delete old allow_address_pair MACs because
        # allow_address_pair MACs will be updated in
        # self.get_or_create_ofport(port)
        if old_of_port:
            LOG.error("Initializing port %s that was already "
                      "initialized.",
                      port['device'])
            self.delete_all_port_flows(old_of_port)
        of_port = self.get_or_create_ofport(port)
        self.initialize_port_flows(of_port)
        self.add_flows_from_rules(of_port)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def update_port_filter(self, port):
        """Update rules for given port

        Current existing filtering rules are removed and new ones are generated
        based on current loaded firewall group rules and members.

        Note: port no security should be handled by security group in
        co-existence mode, otherwise fwg will handle it.

        """
        if not firewall.port_sec_enabled(port) and not self.sg_with_ovs:
            self.remove_port_filter(port)
            self._initialize_egress_no_port_security(port)
            return
        elif not self.is_port_managed(port):
            if not self.sg_with_ovs:
                self._remove_egress_no_port_security(port['device'])
            self.prepare_port_filter(port)
            return

        old_of_port = self.get_ofport(port)
        of_port = self.get_or_create_ofport(port)
        # TODO(jlibosva): Handle firewall blink
        self.delete_all_port_flows(old_of_port)
        self.initialize_port_flows(of_port)
        self.add_flows_from_rules(of_port)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver,
    # sg_port_map -> fwg_port_map
    def remove_port_filter(self, port):
        """Remove port from firewall

        All flows related to this port are removed from ovs. Port is also
        removed from ports managed by this firewall.

        """
        if self.is_port_managed(port):
            of_port = self.get_ofport(port)
            self.delete_all_port_flows(of_port)
            self.fwg_port_map.remove_port(of_port)
            self._schedule_fwg_deletion_maybe(of_port.fw_group.id)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # with ingress\egress rules arguments instead of single rules
    def update_firewall_group_rules(self, fwg_id, ingress_rules, egress_rules):
        self.fwg_port_map.update_rules(fwg_id, ingress_rules, egress_rules)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # with sg_port_map -> fwg_port_map
    def _schedule_fwg_deletion_maybe(self, fwg_id):
        """Schedule possible deletion of the given firewall group.

        This function must be called when the number of ports
        associated to fwg_id drops to zero, as it isn't possible
        to know FWG deletions from agents due to RPC API design.
        """
        fwg_group = self.fwg_port_map.get_or_create_fwg(fwg_id)
        if not fwg_group.members or not fwg_group.ports:
            self.fwg_to_delete.add(fwg_id)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # with sg_port_map -> fwg_port_map
    def _cleanup_stale_fwg(self):
        fwg_to_delete = self.fwg_to_delete
        self.fwg_to_delete = set()

        for fwg_id in fwg_to_delete:
            fw_group = self.fwg_port_map.get_fwg(fwg_id)
            if fw_group.members and fw_group.ports:
                # firewall group is still in use
                continue

            self.fwg_port_map.delete_fwg(fwg_id)

    def process_trusted_ports(self, ports):
        """Pass packets from these ports directly to ingress pipeline."""
        if self.sg_with_ovs:
            return

        for port in ports:
            self._initialize_egress_no_port_security(port)

    def remove_trusted_ports(self, port_ids):
        if self.sg_with_ovs:
            return

        for port_id in port_ids:
            self._remove_egress_no_port_security(port_id)

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def filter_defer_apply_on(self):
        self._deferred = True

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    def filter_defer_apply_off(self):
        if self._deferred:
            self._cleanup_stale_fwg()
            self.int_br.apply_flows()
            self._deferred = False

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # with sg_port_map -> fwg_port_map
    @property
    def ports(self):
        return {id_: port.neutron_port_dict
                for id_, port in self.fwg_port_map.ports.items()}

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def initialize_port_flows(self, port):
        """Set base flows for port

        :param port: OFPort instance

        """
        # Identify egress flow
        self._add_flow(
            table=ovs_consts.TRANSIENT_TABLE,
            priority=105,
            in_port=port.ofport,
            actions='set_field:{:d}->reg{:d},'
                    'set_field:{:d}->reg{:d},'
                    'resubmit(,{:d})'.format(
                        port.ofport,
                        fwaas_ovs_consts.REG_PORT,
                        port.vlan_tag,
                        fwaas_ovs_consts.REG_NET,
                        fwaas_ovs_consts.FW_BASE_EGRESS_TABLE)
        )

        # Identify ingress flows after egress filtering
        for mac_addr in port.all_allowed_macs:
            self._add_flow(
                table=ovs_consts.TRANSIENT_TABLE,
                priority=95,
                dl_dst=mac_addr,
                dl_vlan='0x%x' % port.vlan_tag,
                actions='set_field:{:d}->reg{:d},'
                        'set_field:{:d}->reg{:d},'
                        'strip_vlan,resubmit(,{:d})'.format(
                            port.ofport,
                            fwaas_ovs_consts.REG_PORT,
                            port.vlan_tag,
                            fwaas_ovs_consts.REG_NET,
                            fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
            )

        self._initialize_egress(port)
        self._initialize_ingress(port)

    def _fwaas_process_colocated_ingress(self, port):
        for mac_addr in port.all_allowed_macs:
            self._add_flow(
                table=ovs_consts.ACCEPT_OR_INGRESS_TABLE,
                priority=105,
                dl_dst=mac_addr,
                reg_net=port.vlan_tag,
                actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format(
                    port.ofport,
                    fwaas_ovs_consts.REG_PORT,
                    fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
            )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_egress_ipv6_icmp(self, port):
        for icmp_type in firewall.ICMPV6_ALLOWED_EGRESS_TYPES:
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=95,
                in_port=port.ofport,
                reg_port=port.ofport,
                dl_type=lib_const.ETHERTYPE_IPV6,
                nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
                icmp_type=icmp_type,
                actions='normal')

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers) and exception classes
    def _initialize_egress_no_port_security(self, port):
        port_id = port['device']
        try:
            ovs_port = self.get_ovs_port(port_id)
            vlan_tag = self._get_port_vlan_tag(port)
        except exceptions.OVSFWaaSTagNotFound:
            # It's a patch port, don't set anything
            return
        except exceptions.OVSFWaaSPortNotFound as not_found_e:
            LOG.error("Initializing unfiltered port %(port_id)s that does not "
                      "exist in ovsdb: %(err)s.",
                      {'port_id': port_id,
                       'err': not_found_e})
            return
        self.fwg_port_map.unfiltered[port_id] = ovs_port.ofport
        self._add_flow(
            table=ovs_consts.TRANSIENT_TABLE,
            priority=100,
            in_port=ovs_port.ofport,
            actions='set_field:%d->reg%d,'
                    'set_field:%d->reg%d,'
                    'resubmit(,%d)' % (
                        ovs_port.ofport,
                        fwaas_ovs_consts.REG_PORT,
                        vlan_tag,
                        fwaas_ovs_consts.REG_NET,
                        fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE)
        )
        self._add_flow(
            table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
            priority=80,
            reg_port=ovs_port.ofport,
            actions='normal',
        )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _remove_egress_no_port_security(self, port_id):
        try:
            ofport = self.fwg_port_map.unfiltered[port_id]
        except KeyError:
            LOG.debug("Port %s is not handled by the firewall.", port_id)
            return
        self._delete_flows(
            table=ovs_consts.TRANSIENT_TABLE,
            in_port=ofport
        )
        self._delete_flows(
            table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
            reg_port=ofport
        )
        del self.fwg_port_map.unfiltered[port_id]

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_egress(self, port):
        """Identify egress traffic and send it to egress base"""
        self._initialize_egress_ipv6_icmp(port)

        # Apply mac/ip pairs for IPv4
        allowed_pairs = port.allowed_pairs_v4.union(
            {(port.mac, ip_addr) for ip_addr in port.ipv4_addresses})
        for mac_addr, ip_addr in allowed_pairs:
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=95,
                in_port=port.ofport,
                reg_port=port.ofport,
                dl_src=mac_addr,
                dl_type=lib_const.ETHERTYPE_ARP,
                arp_spa=ip_addr,
                actions='normal'
            )
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=65,
                reg_port=port.ofport,
                ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
                dl_type=lib_const.ETHERTYPE_IP,
                in_port=port.ofport,
                dl_src=mac_addr,
                nw_src=ip_addr,
                actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
                    fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
                    fwaas_ovs_consts.REG_NET)
            )

        # Apply mac/ip pairs for IPv6
        allowed_pairs = port.allowed_pairs_v6.union(
            {(port.mac, ip_addr) for ip_addr in port.ipv6_addresses})
        for mac_addr, ip_addr in allowed_pairs:
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=65,
                reg_port=port.ofport,
                in_port=port.ofport,
                ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
                dl_type=lib_const.ETHERTYPE_IPV6,
                dl_src=mac_addr,
                ipv6_src=ip_addr,
                actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
                    fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
                    fwaas_ovs_consts.REG_NET)
            )

        # DHCP discovery
        accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE
        if self.sg_with_ovs:
            accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE
        for dl_type, src_port, dst_port in (
                (lib_const.ETHERTYPE_IP, 68, 67),
                (lib_const.ETHERTYPE_IPV6, 546, 547)):
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=80,
                reg_port=port.ofport,
                in_port=port.ofport,
                dl_type=dl_type,
                nw_proto=lib_const.PROTO_NUM_UDP,
                tp_src=src_port,
                tp_dst=dst_port,
                actions=f'resubmit(,{accept_or_ingress:d})'
            )
        # Ban dhcp service running on an instance
        for dl_type, src_port, dst_port in (
                (lib_const.ETHERTYPE_IP, 67, 68),
                (lib_const.ETHERTYPE_IPV6, 547, 546)):
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
                priority=70,
                in_port=port.ofport,
                reg_port=port.ofport,
                dl_type=dl_type,
                nw_proto=lib_const.PROTO_NUM_UDP,
                tp_src=src_port,
                tp_dst=dst_port,
                actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
            )

        # Drop Router Advertisements from instances
        self._add_flow(
            table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
            priority=70,
            in_port=port.ofport,
            reg_port=port.ofport,
            dl_type=lib_const.ETHERTYPE_IPV6,
            nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
            icmp_type=lib_const.ICMPV6_TYPE_RA,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )

        # Drop all remaining not tracked egress connections
        self._add_flow(
            table=fwaas_ovs_consts.FW_BASE_EGRESS_TABLE,
            priority=10,
            ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
            in_port=port.ofport,
            reg_port=port.ofport,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )

        # Fill in accept_or_ingress table by checking that traffic is ingress
        # and if not, accept it
        if self.sg_with_ovs:
            self._fwaas_process_colocated_ingress(port)
        else:
            for mac_addr in port.all_allowed_macs:
                self._add_flow(
                    table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
                    priority=100,
                    dl_dst=mac_addr,
                    reg_net=port.vlan_tag,
                    actions='set_field:{:d}->reg{:d},resubmit(,{:d})'.format(
                        port.ofport,
                        fwaas_ovs_consts.REG_PORT,
                        fwaas_ovs_consts.FW_BASE_INGRESS_TABLE),
                )
            for ethertype in [lib_const.ETHERTYPE_IP,
                              lib_const.ETHERTYPE_IPV6]:
                self._add_flow(
                    table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
                    priority=90,
                    dl_type=ethertype,
                    reg_port=port.ofport,
                    ct_state=fwaas_ovs_consts.OF_STATE_NEW_NOT_ESTABLISHED,
                    actions='ct(commit,zone=NXM_NX_REG{:d}[0..15]),'
                            'resubmit(,{:d})'.format(
                                fwaas_ovs_consts.REG_NET,
                                ovs_consts.ACCEPTED_EGRESS_TRAFFIC_TABLE)
                )
            self._add_flow(
                table=fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE,
                priority=80,
                reg_port=port.ofport,
                actions='normal'
            )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_tracked_egress(self, port):
        # Drop invalid packets
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
            priority=50,
            ct_state=fwaas_ovs_consts.OF_STATE_INVALID,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )
        # Drop traffic for removed fwg rules
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
            priority=50,
            reg_port=port.ofport,
            ct_mark=fwaas_ovs_consts.CT_MARK_INVALID,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )

        for state in (
            fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY,
            fwaas_ovs_consts.OF_STATE_RELATED,
        ):
            self._add_flow(
                table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
                priority=50,
                ct_state=state,
                ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL,
                reg_port=port.ofport,
                ct_zone=port.vlan_tag,
                actions='normal'
            )
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
            priority=40,
            reg_port=port.ofport,
            ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )
        for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]:
            self._add_flow(
                table=fwaas_ovs_consts.FW_RULES_EGRESS_TABLE,
                priority=40,
                dl_type=ethertype,
                reg_port=port.ofport,
                ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED,
                actions="ct(commit,zone=NXM_NX_REG{:d}[0..15],"
                        "exec(set_field:{:s}->ct_mark))".format(
                            fwaas_ovs_consts.REG_NET,
                            fwaas_ovs_consts.CT_MARK_INVALID)
            )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_ingress_ipv6_icmp(self, port):
        for icmp_type in firewall.ICMPV6_ALLOWED_INGRESS_TYPES:
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
                priority=100,
                reg_port=port.ofport,
                dl_dst=port.mac,
                dl_type=lib_const.ETHERTYPE_IPV6,
                nw_proto=lib_const.PROTO_NUM_IPV6_ICMP,
                icmp_type=icmp_type,
                actions=f'output:{port.ofport:d}'
            )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_ingress(self, port):
        # Allow incoming ARPs
        self._add_flow(
            table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
            priority=100,
            dl_type=lib_const.ETHERTYPE_ARP,
            reg_port=port.ofport,
            actions=f'output:{port.ofport:d}'
        )
        self._initialize_ingress_ipv6_icmp(port)

        # DHCP offers
        for dl_type, src_port, dst_port in (
                (lib_const.ETHERTYPE_IP, 67, 68),
                (lib_const.ETHERTYPE_IPV6, 547, 546)):
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
                priority=95,
                reg_port=port.ofport,
                dl_type=dl_type,
                nw_proto=lib_const.PROTO_NUM_UDP,
                tp_src=src_port,
                tp_dst=dst_port,
                actions=f'output:{port.ofport:d}'
            )

        # Track untracked
        for dl_type in (lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6):
            self._add_flow(
                table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
                priority=90,
                reg_port=port.ofport,
                dl_type=dl_type,
                ct_state=fwaas_ovs_consts.OF_STATE_NOT_TRACKED,
                actions='ct(table={:d},zone=NXM_NX_REG{:d}[0..15])'.format(
                    fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
                    fwaas_ovs_consts.REG_NET)
            )
        self._add_flow(
            table=fwaas_ovs_consts.FW_BASE_INGRESS_TABLE,
            ct_state=fwaas_ovs_consts.OF_STATE_TRACKED,
            priority=80,
            reg_port=port.ofport,
            actions='resubmit(,{:d})'.format(
                fwaas_ovs_consts.FW_RULES_INGRESS_TABLE)
        )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def _initialize_tracked_ingress(self, port):
        # Drop invalid packets
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
            priority=50,
            ct_state=fwaas_ovs_consts.OF_STATE_INVALID,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )
        # Drop traffic for removed fwg rules
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
            priority=50,
            reg_port=port.ofport,
            ct_mark=fwaas_ovs_consts.CT_MARK_INVALID,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )

        # Allow established and related connections
        for state in (fwaas_ovs_consts.OF_STATE_ESTABLISHED_REPLY,
                      fwaas_ovs_consts.OF_STATE_RELATED):
            self._add_flow(
                table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
                priority=50,
                reg_port=port.ofport,
                ct_state=state,
                ct_mark=fwaas_ovs_consts.CT_MARK_NORMAL,
                ct_zone=port.vlan_tag,
                actions=f'output:{port.ofport:d}'
            )
        self._add_flow(
            table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
            priority=40,
            reg_port=port.ofport,
            ct_state=fwaas_ovs_consts.OF_STATE_NOT_ESTABLISHED,
            actions='resubmit(,%d)' % ovs_consts.DROPPED_TRAFFIC_TABLE
        )
        for ethertype in [lib_const.ETHERTYPE_IP, lib_const.ETHERTYPE_IPV6]:
            self._add_flow(
                table=fwaas_ovs_consts.FW_RULES_INGRESS_TABLE,
                priority=40,
                dl_type=ethertype,
                reg_port=port.ofport,
                ct_state=fwaas_ovs_consts.OF_STATE_ESTABLISHED,
                actions="ct(commit,zone=NXM_NX_REG{:d}[0..15],"
                        "exec(set_field:{:s}->ct_mark))".format(
                            fwaas_ovs_consts.REG_NET,
                            fwaas_ovs_consts.CT_MARK_INVALID)
            )

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers) and rules_generator method
    def add_flows_from_rules(self, port):
        self._initialize_tracked_ingress(port)
        self._initialize_tracked_egress(port)
        LOG.debug('Creating flow rules for port %s that is port %d in OVS',
                  port.id, port.ofport)
        for rule in self.create_rules_generator_for_port(port):
            flows = rules.create_flows_from_rule_and_port(rule, port)
            LOG.debug("RULGEN: Rules generated for flow %s are %s",
                      rule, flows)
            for flow in flows:
                if rule.get('action') == ACTION_ALLOW:
                    self._accept_flow(**flow)
                else:
                    self._drop_flow(**flow)

    def create_rules_generator_for_port(self, port):
        """Returns a generator emitting rules valid for further processing

        Injects necessary fields to feed one-by-one to rules module to
        transform into valid openflow rules.
        """

        def inject_fields(rule, direction, offset=0):
            """Add fields to rule dict to be able to utilize rules module

            Currently such fields are added:
            'offset', 'direction', 'ethertype', 'source_port_range_min',
            'source_port_range_max', 'port_range_min', 'port_range_max'
            """
            # XXX NOTE(ivasilevskaya) maybe there's a clever way to do that
            version_ethertype_map = {lib_const.IP_VERSION_4: lib_const.IPv4,
                                     lib_const.IP_VERSION_6: lib_const.IPv6}

            rule['direction'] = direction
            rule['ethertype'] = version_ethertype_map[rule['ip_version']]
            rule['offset'] = offset

            # transfer destination_port into port_range_min/port_range_max
            def add_range(range_key, key_min, key_max):
                range_str = rule.get(range_key)
                if not range_str:
                    return
                ports = range_str.split(':', 1)
                rule[key_min] = int(ports[0])
                rule[key_max] = (
                    int(ports[1]) if len(ports) == 2 else int(ports[0]))

            add_range('destination_port', 'port_range_min', 'port_range_max')
            add_range('source_port', 'source_port_range_min',
                      'source_port_range_max')

        # add direction field
        offset = len(port.fw_group.ingress_rules) - 1
        for rule in port.fw_group.ingress_rules:
            inject_fields(rule, lib_const.INGRESS_DIRECTION, offset)
            offset -= 1
            yield rule

        offset = len(port.fw_group.egress_rules) - 1
        for rule in port.fw_group.egress_rules:
            inject_fields(rule, lib_const.EGRESS_DIRECTION, offset)
            offset -= 1
            yield rule

    # NOTE(ivasilevskaya) That's a copy-paste from neutron ovsfw driver
    # which differs in constants (table numbers)
    def delete_all_port_flows(self, port):
        """Delete all flows for given port"""
        accept_or_ingress = fwaas_ovs_consts.FW_ACCEPT_OR_INGRESS_TABLE
        if self.sg_with_ovs:
            accept_or_ingress = ovs_consts.ACCEPT_OR_INGRESS_TABLE

        for mac_addr in port.all_allowed_macs:
            self._strict_delete_flow(priority=95,
                                     table=ovs_consts.TRANSIENT_TABLE,
                                     dl_dst=mac_addr,
                                     dl_vlan=port.vlan_tag)
            self._delete_flows(
                table=accept_or_ingress,
                dl_dst=mac_addr, reg_net=port.vlan_tag)
        self._strict_delete_flow(priority=105,
                                 table=ovs_consts.TRANSIENT_TABLE,
                                 in_port=port.ofport)
        self._delete_flows(reg_port=port.ofport)

    def create_firewall_group(self, ports_for_fwg, firewall_group):
        egress_rules = firewall_group['egress_rule_list']
        ingress_rules = firewall_group['ingress_rule_list']
        fwg_id = firewall_group['id']

        self.update_firewall_group_rules(fwg_id, ingress_rules, egress_rules)
        for port in ports_for_fwg:
            port['firewall_group'] = fwg_id
            self.update_port_filter(port)

    def update_firewall_group(self, ports_for_fwg, firewall_group):
        self.create_firewall_group(ports_for_fwg, firewall_group)

    def delete_firewall_group(self, ports_for_fwg, firewall_group):
        for port in ports_for_fwg:
            port['firewall_group'] = firewall_group['id']
            self.remove_port_filter(port)
