"""Configuration for ACLs."""

# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# 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 copy
import netaddr
from os_ken.ofproto import ether

from faucet import valve_of
from faucet import valve_acl
from faucet.valve_of import MATCH_FIELDS, OLD_MATCH_FIELDS
from faucet.conf import Conf, test_config_condition, InvalidConfigError
from faucet.valve_table import wildcard_table


class ACL(Conf):
    """Contains the state for an ACL, including the configuration.

ACL Config

ACLs are configured under the 'acls' configuration block. The acls block
contains a dictionary of individual acls each keyed by its name.

Each acl contains a list of rules, a packet will have the first matching rule
applied to it.

Each rule is a dictionary containing the single key 'rule' with the value the
matches and actions for the rule.

The matches are key/values based on the ryu RESTFul API.
The key 'actions' contains a dictionary with keys/values as follows:

 * allow (int): if 1 allow the packet to continue through the Faucet \
       pipeline, if 0 drop the packet.
 * force_port_vlan (int): if 1, do not verify the VLAN/port association \
       for this packet and override any VLAN ACL on the forced VLAN.
 * meter (str): meter to apply to the packet
 * output (dict): used to output a packet directly. details below.
 * cookie (int): set flow cookie to this value on this flow

The output action contains a dictionary with the following elements:

 * tunnel (dict): the tunnel formation, creates a tunnel from the applied port(s) \
       to the specified destination
 * port (int or string): the port to output the packet to
 * ports (list): a list of the ports (int or string) to output the packet to
 * set_fields (list): a list of fields to set with values
 * pop_vlans: (int): pop the packet vlan before outputting
 * vlan_vid: (int): push the vlan vid on the packet when outputting
 * vlan_vids: (list): push the list of vlans on the packet when outputting, with option eth_type
 * swap_vid (int): rewrite the vlan vid of the packet when outputting
 * failover (dict): Output with a failover port (experimental)
"""

    defaults = {
        "rules": None,
        "exact_match": False,
        "dot1x_assigned": False,
    }
    defaults_types = {
        "rules": list,
        "exact_match": bool,
        "dot1x_assigned": bool,
    }
    rule_types = {
        "cookie": int,
        "actions": dict,
        "description": str,
    }
    actions_types = {
        "meter": str,
        "mirror": (str, int),
        "output": (dict, list),
        "allow": int,
        "force_port_vlan": int,
        "ct": dict,
    }
    output_actions_types = {
        "tunnel": dict,
        "port": (str, int),
        "ports": list,
        "failover": dict,
        "set_fields": list,
        "pop_vlans": int,
        "swap_vid": int,
        "vlan_vid": int,
        "vlan_vids": list,
    }
    ct_action_types = {
        "flags": int,
        "alg": int,
        "table": int,
        "zone": int,
        "zone_src": int,
        "clear": bool,
        "nat": dict,
        "mark": int,
        "label": int,
    }
    ct_action_nat_types = {
        "flags": int,
        "range_ipv4_min": str,
        "range_ipv4_max": str,
        "range_ipv6_min": str,
        "range_ipv6_max": str,
        "range_proto_min": int,
        "range_proto_max": int,
    }
    tunnel_types = {
        "type": (str, None),
        "tunnel_id": (str, int, None),
        "dp": str,
        "port": (str, int, None),
        "exit_instructions": (list, None),
        "maintain_encapsulation": bool,
        "bi_directional": bool,
        "reverse": bool,
    }

    mutable_attrs = frozenset(["tunnel_sources"])

    def __init__(self, _id, dp_id, conf):
        self.rules = []
        self.exact_match = None
        self.dot1x_assigned = None
        self.meter = False
        self.matches = {}
        self.set_fields = set()
        self._ports_resolved = False

        # Tunnel info maintains the tunnel output information for each tunnel rule
        self.tunnel_dests = {}
        # Tunnel sources is a list of the sources in the network for this ACL
        self.tunnel_sources = {}
        # Tunnel rules is the rules for each tunnel in the ACL for each source
        self.dyn_tunnel_rules = {}
        self.dyn_reverse_tunnel_rules = {}

        for match_fields in (MATCH_FIELDS, OLD_MATCH_FIELDS):
            self.rule_types.update({match: (str, int) for match in match_fields})
        conf = copy.deepcopy(conf)
        if isinstance(conf, dict):
            rules = conf.get("rules", [])
        elif isinstance(conf, list):
            rules = conf
            conf = {}
        else:
            raise InvalidConfigError("ACL conf is an invalid type %s" % _id)
        conf["rules"] = []
        for rule in rules:
            normalized_rule = rule
            if isinstance(rule, dict):
                normalized_rule = rule.get("rule", rule)
                if normalized_rule is None:
                    normalized_rule = {k: v for k, v in rule.items() if v is not None}
            test_config_condition(
                not isinstance(normalized_rule, dict),
                ("ACL rule is %s not %s (%s)" % (type(normalized_rule), dict, rules)),
            )
            conf["rules"].append(normalized_rule)
        super().__init__(_id, dp_id, conf)

    def finalize(self):
        self._ports_resolved = True
        super().finalize()

    def check_config(self):
        test_config_condition(not self.rules, "no rules found for ACL %s" % self._id)
        for rule in self.rules:
            self._check_conf_types(rule, self.rule_types)
            for rule_field, rule_conf in rule.items():
                if rule_field == "cookie":
                    test_config_condition(
                        rule_conf < 0 or rule_conf > 2**16,
                        "rule cookie value must be 0-2**16",
                    )
                elif rule_field == "actions":
                    test_config_condition(
                        not rule_conf, "Missing rule actions in ACL %s" % self._id
                    )
                    self._check_conf_types(rule_conf, self.actions_types)
                    for action_name, action_conf in rule_conf.items():
                        if action_name == "output":
                            if isinstance(action_conf, (list, tuple)):
                                # New ordered format
                                for subconf in action_conf:
                                    # Make sure only one specified action per list element
                                    test_config_condition(
                                        len(subconf) > 1,
                                        "ACL ordered output must have only one action per element",
                                    )
                                    # Ensure correct action format
                                    self._check_conf_types(
                                        subconf, self.output_actions_types
                                    )
                            else:
                                # Old format
                                self._check_conf_types(
                                    action_conf, self.output_actions_types
                                )
                        elif action_name == "ct":
                            self._check_conf_types(action_conf, self.ct_action_types)
                            # if clear set, make sure nothing else is
                            if "clear" in action_conf and action_conf["clear"]:
                                test_config_condition(
                                    len(action_conf) != 1,
                                    "no other parameters can be set when 'clear' set on "
                                    "conntrack ACL",
                                )
                            else:
                                test_config_condition(
                                    "table" not in action_conf,
                                    "required parameter 'table' not set for conntrack ACL",
                                )
                                test_config_condition(
                                    "zone" not in action_conf,
                                    "required parameter 'zone' not set for conntrack ACL",
                                )
                            if "nat" in action_conf:
                                self._check_conf_types(
                                    action_conf["nat"], self.ct_action_nat_types
                                )

    def build(self, meters, vid, port_num):
        """Check that ACL can be built from config."""

        self.matches = {}
        self.set_fields = set()
        self.meter = False
        if self.rules:
            try:
                ofmsgs = valve_acl.build_acl_ofmsgs(
                    [self],
                    wildcard_table,
                    [valve_of.goto_table(wildcard_table)],
                    [valve_of.goto_table(wildcard_table)],
                    2**16 - 1,
                    meters,
                    self.exact_match,
                    vlan_vid=vid,
                    port_num=port_num,
                )
            except (netaddr.core.AddrFormatError, KeyError, ValueError) as err:
                raise InvalidConfigError from err
            test_config_condition(not ofmsgs, "OF messages is empty")
            for ofmsg in ofmsgs:
                try:
                    valve_of.verify_flowmod(ofmsg)
                except (KeyError, ValueError) as err:
                    raise InvalidConfigError from err
                except Exception as err:
                    raise err
                if valve_of.is_flowmod(ofmsg):
                    apply_actions = []
                    for inst in ofmsg.instructions:
                        if valve_of.is_apply_actions(inst):
                            apply_actions.extend(inst.actions)
                        elif valve_of.is_meter(inst):
                            self.meter = True
                    for action in apply_actions:
                        if valve_of.is_set_field(action):
                            self.set_fields.add(action.key)
                    for match, value in ofmsg.match.items():
                        has_mask = isinstance(value, (tuple, list))
                        if has_mask or match not in self.matches:
                            self.matches[match] = has_mask
        for tunnel_rules in self.tunnel_dests.values():
            if "exit_instructions" in tunnel_rules:
                exit_inst = tunnel_rules["exit_instructions"]
                try:
                    ofmsgs = valve_acl.build_tunnel_ofmsgs(exit_inst, wildcard_table, 1)
                except (netaddr.core.AddrFormatError, KeyError, ValueError) as err:
                    raise InvalidConfigError from err
                test_config_condition(not ofmsgs, "OF messages is empty")
                for ofmsg in ofmsgs:
                    try:
                        valve_of.verify_flowmod(ofmsg)
                    except (KeyError, ValueError) as err:
                        raise InvalidConfigError from err
                    except Exception as err:
                        raise err
                    if valve_of.is_flowmod(ofmsg):
                        apply_actions = []
                        for inst in ofmsg.instructions:
                            if valve_of.is_apply_actions(inst):
                                apply_actions.extend(inst.actions)
                            elif valve_of.is_meter(inst):
                                self.meter = True
                        for action in apply_actions:
                            if valve_of.is_set_field(action):
                                self.set_fields.add(action.key)
                        for match, value in ofmsg.match.items():
                            has_mask = isinstance(value, (tuple, list))
                            if has_mask or match not in self.matches:
                                self.matches[match] = has_mask
        return (self.matches, self.set_fields, self.meter)

    def get_meters(self):
        """Yield meters for each rule in ACL"""
        for rule in self.rules:
            if "actions" not in rule or "meter" not in rule["actions"]:
                continue
            yield rule["actions"]["meter"]

    def get_mirror_destinations(self):
        """Yield mirror destinations for each rule in ACL"""
        for rule in self.rules:
            if "actions" not in rule or "mirror" not in rule["actions"]:
                continue
            yield rule["actions"]["mirror"]

    def _resolve_ordered_output_ports(
        self, output_list, resolve_port_cb, resolve_tunnel_objects
    ):
        """Resolve output actions in the ordered list format"""
        result = []
        for action in output_list:
            for key, value in action.items():
                if key == "tunnel":
                    tunnel = value
                    # Fetch tunnel items from the tunnel output dict
                    test_config_condition(
                        "dp" not in tunnel, "ACL (%s) tunnel DP not defined" % self._id
                    )
                    tunnel_dp = tunnel["dp"]
                    tunnel_port = tunnel.get("port", None)
                    tunnel_id = tunnel.get("tunnel_id", None)
                    tunnel_type = tunnel.get("type", "vlan")
                    tunnel_exit_instructions = tunnel.get("exit_instructions", [])
                    tunnel_direction = tunnel.get("bi_directional", False)
                    tunnel_maintain = tunnel.get("maintain_encapsulation", False)
                    tunnel_reverse = tunnel.get("reverse", False)
                    test_config_condition(
                        tunnel_reverse and tunnel_direction,
                        (
                            "Tunnel ACL %s cannot contain values for the fields"
                            "`bi_directional` and `reverse` at the same time" % self._id
                        ),
                    )
                    # Resolve the tunnel items
                    dst_dp, dst_port, tunnel_id = resolve_tunnel_objects(
                        tunnel_dp, tunnel_port, tunnel_id
                    )
                    # Compile the tunnel into an easy-access dictionary
                    tunnel_dict = {
                        "dst_dp": dst_dp,
                        "dst_port": dst_port,
                        "tunnel_id": tunnel_id,
                        "type": tunnel_type,
                        "exit_instructions": tunnel_exit_instructions,
                        "bi_directional": tunnel_direction,
                        "maintain_encapsulation": tunnel_maintain,
                        "reverse": tunnel_reverse,
                    }
                    self.tunnel_dests[tunnel_id] = tunnel_dict
                    result.append({key: tunnel_id})
                elif key == "port":
                    port_name = value
                    port = resolve_port_cb(port_name)
                    test_config_condition(
                        not port,
                        "ACL (%s) output port undefined in DP: %s"
                        % (self._id, self.dp_id),
                    )
                    result.append({key: port})
                elif key == "ports":
                    resolved_ports = [resolve_port_cb(p) for p in value]
                    test_config_condition(
                        None in resolved_ports,
                        "ACL (%s) output port(s) not defined in DP: %s"
                        % (self._id, self.dp_id),
                    )
                    result.append({key: resolved_ports})
                elif key == "failover":
                    failover = value
                    test_config_condition(
                        not isinstance(failover, dict), ("failover is not a dictionary")
                    )
                    failover_dict = {}
                    for failover_name, failover_values in failover.items():
                        if failover_name == "ports":
                            resolved_ports = [
                                resolve_port_cb(p) for p in failover_values
                            ]
                            test_config_condition(
                                None in resolved_ports,
                                "ACL (%s) failover port(s) not defined in DP: %s"
                                % (self._id, self.dp_id),
                            )
                            failover_dict[failover_name] = resolved_ports
                        else:
                            failover_dict[failover_name] = failover_values
                    result.append({key: failover_dict})
                else:
                    result.append(action)
        return result

    def _resolve_output_ports(
        self, action_conf, resolve_port_cb, resolve_tunnel_objects
    ):
        """Resolve the values for output actions in the ACL"""
        if isinstance(action_conf, (list, tuple)):
            return self._resolve_ordered_output_ports(
                action_conf, resolve_port_cb, resolve_tunnel_objects
            )
        result = {}
        test_config_condition(
            "vlan_vid" in action_conf and "vlan_vids" in action_conf,
            "ACL %s has both vlan_vid and vlan_vids defined" % self._id,
        )
        test_config_condition(
            "port" in action_conf and "ports" in action_conf,
            "ACL %s has both port and ports defined" % self._id,
        )
        for output_action, output_action_values in action_conf.items():
            if output_action == "tunnel":
                tunnel = output_action_values
                # Fetch tunnel items from the tunnel output dict
                test_config_condition(
                    "dp" not in tunnel, "ACL (%s) tunnel DP not defined" % self._id
                )
                tunnel_dp = tunnel["dp"]
                tunnel_port = tunnel.get("port", None)
                tunnel_id = tunnel.get("tunnel_id", None)
                tunnel_type = tunnel.get("type", "vlan")
                tunnel_exit_instructions = tunnel.get("exit_instructions", [])
                tunnel_direction = tunnel.get("bi_directional", False)
                tunnel_maintain = tunnel.get("maintain_encapsulation", False)
                tunnel_reverse = tunnel.get("reverse", False)
                test_config_condition(
                    tunnel_reverse and tunnel_direction,
                    (
                        "Tunnel ACL %s cannot contain values for the fields"
                        "`bi_directional` and `reverse` at the same time" % self._id
                    ),
                )
                # Resolve the tunnel items
                dst_dp, dst_port, tunnel_id = resolve_tunnel_objects(
                    tunnel_dp, tunnel_port, tunnel_id
                )
                # Compile the tunnel into an easy-access dictionary
                tunnel_dict = {
                    "dst_dp": dst_dp,
                    "dst_port": dst_port,
                    "tunnel_id": tunnel_id,
                    "type": tunnel_type,
                    "exit_instructions": tunnel_exit_instructions,
                    "bi_directional": tunnel_direction,
                    "maintain_encapsulation": tunnel_maintain,
                    "reverse": tunnel_reverse,
                }
                self.tunnel_dests[tunnel_id] = tunnel_dict
                result[output_action] = tunnel_id
            elif output_action == "port":
                port_name = output_action_values
                port = resolve_port_cb(port_name)
                test_config_condition(
                    not port,
                    (
                        "ACL (%s) output port undefined in DP: %s"
                        % (self._id, self.dp_id)
                    ),
                )
                result[output_action] = port
            elif output_action == "ports":
                resolved_ports = [resolve_port_cb(p) for p in output_action_values]
                test_config_condition(
                    None in resolved_ports,
                    (
                        "ACL (%s) output port(s) not defined in DP: %s"
                        % (self._id, self.dp_id)
                    ),
                )
                result[output_action] = resolved_ports
            elif output_action == "failover":
                failover = output_action_values
                test_config_condition(
                    not isinstance(failover, dict), ("failover is not a dictionary")
                )
                result[output_action] = {}
                for failover_name, failover_values in failover.items():
                    if failover_name == "ports":
                        resolved_ports = [resolve_port_cb(p) for p in failover_values]
                        test_config_condition(
                            None in resolved_ports,
                            (
                                "ACL (%s) failover port(s) not defined in DP: %s"
                                % (self._id, self.dp_id)
                            ),
                        )
                        result[output_action][failover_name] = resolved_ports
                    else:
                        result[output_action][failover_name] = failover_values
            else:
                result[output_action] = output_action_values
        return result

    def resolve_ports(self, resolve_port_cb, resolve_tunnel_objects):
        """Resolve the values for the actions of an ACL"""
        if self._ports_resolved:
            return
        for rule_conf in self.rules:
            if "actions" in rule_conf:
                actions_conf = rule_conf["actions"]
                resolved_actions = {}
                test_config_condition(
                    not isinstance(actions_conf, dict),
                    ("actions value is not a dictionary"),
                )
                for action_name, action_conf in actions_conf.items():
                    if action_name == "mirror":
                        resolved_port = resolve_port_cb(action_conf)
                        test_config_condition(
                            resolved_port is None,
                            (
                                "ACL (%s) mirror port is not defined in DP: %s"
                                % (self._id, self.dp_id)
                            ),
                        )
                        resolved_actions[action_name] = resolved_port
                    elif action_name == "output":
                        resolved_action = self._resolve_output_ports(
                            action_conf, resolve_port_cb, resolve_tunnel_objects
                        )
                        resolved_actions[action_name] = resolved_action
                    else:
                        resolved_actions[action_name] = action_conf
                rule_conf["actions"] = resolved_actions
        self._ports_resolved = True

    def requires_reverse_tunnel(self, tunnel_id):
        """Returns true if the tunnel requires a reverse pathway"""
        return self.tunnel_dests[tunnel_id]["bi_directional"]

    def get_num_tunnels(self):
        """Returns the number of tunnels specified in the ACL"""
        num_tunnels = 0
        for rule_conf in self.rules:
            if self.does_rule_contain_tunnel(rule_conf):
                output_conf = rule_conf["actions"]["output"]
                if isinstance(output_conf, list):
                    for action in output_conf:
                        for key in action:
                            if key == "tunnel":
                                num_tunnels += 1
                else:
                    if "tunnel" in output_conf:
                        num_tunnels += 1
        return num_tunnels

    def get_tunnel_rules(self, tunnel_id):
        """Return the list of rules that apply a specific tunnel ID"""
        rules = []
        for rule_conf in self.rules:
            if self.does_rule_contain_tunnel(rule_conf):
                output_conf = rule_conf["actions"]["output"]
                if isinstance(output_conf, (list, tuple)):
                    for action in output_conf:
                        for key, value in action.items():
                            if key == "tunnel" and value == tunnel_id:
                                rules.append(rule_conf)
                                continue
                else:
                    if output_conf["tunnel"] == tunnel_id:
                        rules.append(rule_conf)
        return rules

    @staticmethod
    def does_rule_contain_tunnel(rule_conf):
        """Return true if the ACL rule contains a tunnel"""
        if "actions" in rule_conf:
            if "output" in rule_conf["actions"]:
                output_conf = rule_conf["actions"]["output"]
                if isinstance(output_conf, (list, tuple)):
                    for action in output_conf:
                        for key in action:
                            if key == "tunnel":
                                return True
                else:
                    if "tunnel" in output_conf:
                        return True
        return False

    def is_tunnel_acl(self):
        """Return true if the ACL contains a tunnel"""
        if self.tunnel_dests:
            return True
        for rule_conf in self.rules:
            if self.does_rule_contain_tunnel(rule_conf):
                return True
        return False

    @staticmethod
    def _tunnel_source_id(source):
        """Return ID for a tunnel source."""
        return tuple(sorted(source.items()))

    def add_tunnel_source(self, dp_name, port, reverse=False, bi_directional=False):
        """Add a source dp/port pair for the tunnel ACL"""
        source = {
            "dp": dp_name,
            "port": port,
            "reverse": reverse,
            "bi_directional": bi_directional,
        }
        source_id = self._tunnel_source_id(source)
        self.tunnel_sources[source_id] = source
        for _id in self.tunnel_dests:
            self.dyn_tunnel_rules.setdefault(_id, {})
            self.dyn_reverse_tunnel_rules.setdefault(_id, {})

    def verify_tunnel_rules(self):
        """Make sure that matches & set fields are configured correctly to handle tunnels"""
        if "eth_type" not in self.matches:
            self.matches["eth_type"] = False
        if "in_port" not in self.matches:
            self.matches["in_port"] = False
        if "vlan_vid" not in self.matches:
            self.matches["vlan_vid"] = False
        if "vlan_vid" not in self.set_fields:
            self.set_fields.add("vlan_vid")
        if "vlan_pcp" not in self.matches:
            self.matches["vlan_pcp"] = False
        if "vlan_pcp" not in self.set_fields:
            self.set_fields.add("vlan_pcp")

    def update_reverse_tunnel_rules(
        self, curr_dp, source_id, tunnel_id, out_port, output_table
    ):
        """Update the tunnel rulelist for when the output port has changed (reverse direction)"""
        if not self.requires_reverse_tunnel(tunnel_id):
            return False
        dst_dp = self.tunnel_sources[source_id]["dp"]
        src_dp = self.tunnel_dests[tunnel_id]["dst_dp"]
        prev_list = self.dyn_reverse_tunnel_rules[tunnel_id].get(source_id, [])
        new_list = []
        if curr_dp == src_dp and curr_dp != dst_dp:
            # SRC DP: vlan_vid, vlan_pcp, actions=[out_port]
            # NOTE: For the bi_directional reverse tunnel, we assume that
            #       the packet already has the required encapsulation
            new_list = [{"port": out_port}]
        elif curr_dp == dst_dp and curr_dp != src_dp:
            # DST DP: vlan_vid, vlan_pcp, actions=[pop_vlans, output]
            new_list = [{"pop_vlans": 1}]
            if out_port is None:
                # DP dest tunnel, so we fall through into the eth_dst output table
                new_list.append({"goto": output_table.table_id})
            else:
                # Tunnel has port specified, so output to destination
                new_list.append({"port": out_port})
        elif curr_dp == src_dp and curr_dp == dst_dp:
            # SINGLE DP: actions=[pop_vlans, out_port]
            new_list = [{"pop_vlans": 1}]
            if out_port is None:
                # DP dest tunnel, so we fall through into the eth_dst output table
                new_list.extend([{"goto": output_table.table_id}])
            else:
                # Tunnel has port specified, so output to destination
                new_list.extend([{"port": out_port}])
        else:
            # TRANSIT DP: vlan_vid, vlan_pcp, actions=[output]
            new_list = [{"port": out_port}]
        if new_list != prev_list:
            self.dyn_reverse_tunnel_rules[tunnel_id][source_id] = new_list
            return True
        return True

    def update_source_tunnel_rules(
        self, curr_dp, source_id, tunnel_id, out_port, output_table
    ):
        """Update the tunnel rulelist for when the output port has changed"""
        src_dp = self.tunnel_sources[source_id]["dp"]
        dst_dp = self.tunnel_dests[tunnel_id]["dst_dp"]
        prev_list = self.dyn_tunnel_rules[tunnel_id].get(source_id, [])
        new_list = []
        pcp_flag = valve_of.PCP_TUNNEL_FLAG
        if self.tunnel_dests[tunnel_id]["reverse"]:
            pcp_flag = valve_of.PCP_TUNNEL_REVERSE_DIRECTION_FLAG
        if curr_dp == src_dp and curr_dp != dst_dp:
            # SRC DP: in_port, actions=[push_vlan, output, pop_vlans]
            # Ideally, we would be able to detect if the tunnel has an `allow` action clause.
            #   However, this is difficult as a single ACL can have multiple rules using the same
            #   tunnel, but with one instance requiring the `allow` clause and another, not.
            # This means it is easier to always append the `pop_vlans` in assumption that the
            #   `allow` action does exist, and then optimize/reduce the redundant rules before
            #   outputting the flowrule.
            # We also set the tunnel VLAN header with a PCP value indicating that we are in
            #   the tunnel, which will save the VLANs from being reserved.
            new_list = [
                {"vlan_vids": [{"vid": tunnel_id, "eth_type": ether.ETH_TYPE_8021Q}]},
                {"set_fields": [{"vlan_pcp": pcp_flag}]},
                {"port": out_port},
                {"pop_vlans": 1},
            ]
        elif curr_dp == dst_dp and curr_dp != src_dp:
            # DST DP: in_port, vlan_vid, actions=[pop_vlan, additional_instructions, output]
            # If exit_instructions are applied, then we want to pop off the tunnel
            #   VLAN header, then apply the additional instructions, then output
            if self.tunnel_dests[tunnel_id]["maintain_encapsulation"]:
                # We wish to maintain tunnel encapsulation before outputting
                #   So do not add the pop_vlans rule
                new_list = []
            else:
                new_list = [{"pop_vlans": 1}]
            exit_instructions = self.tunnel_dests[tunnel_id].get(
                "exit_instructions", []
            )
            new_list.extend(copy.copy(list(exit_instructions)))
            if out_port is None:
                # DP dest tunnel, so we fall through into the eth_dst output table
                new_list.append({"goto": output_table.table_id})
            else:
                # Tunnel has port specified, so output to destination
                new_list.append({"port": out_port})
        elif curr_dp == src_dp and curr_dp == dst_dp:
            # SINGLE DP: in_port, actions=[additional_instructions, out_port]
            exit_instructions = self.tunnel_dests[tunnel_id].get(
                "exit_instructions", []
            )
            new_list.extend(copy.copy(list(exit_instructions)))
            if self.tunnel_dests[tunnel_id].get("maintain_encapsulation", False):
                # Maintain encapsulation implies we want the tunnel VID on the packet,
                #   so ensure it is purposefully put onto the packet, even when
                #   there would originally be no need to push on a tunnel VID
                new_list.extend(
                    [
                        {
                            "vlan_vids": [
                                {"vid": tunnel_id, "eth_type": ether.ETH_TYPE_8021Q}
                            ]
                        },
                        {"set_fields": [{"vlan_pcp": pcp_flag}]},
                    ]
                )
            if out_port is None:
                # DP dest tunnel, so we fall through into the eth_dst output table
                new_list.extend([{"goto": output_table.table_id}])
            else:
                # Tunnel has port specified, so output to destination
                new_list.extend([{"port": out_port}])
        else:
            # TRANSIT DP: in_port, vlan_vid, actions=[output]
            new_list = [{"port": out_port}]
        if new_list != prev_list:
            self.dyn_tunnel_rules[tunnel_id][source_id] = new_list
            return True
        return True


# NOTE: 802.1x steals the port ACL table.
PORT_ACL_8021X = ACL(
    "port_acl_8021x",
    0,
    {
        "rules": [
            {
                "eth_type": 1,
                "eth_src": "01:02:03:04:05:06",
                "actions": {
                    "output": {
                        "port": valve_of.ofp.OFPP_LOCAL,
                        "set_fields": [
                            {"eth_src": "01:02:03:04:05:06"},
                            {"eth_dst": "01:02:03:04:05:06"},
                        ],
                    }
                },
            }
        ]
    },
)
PORT_ACL_8021X.build({}, None, 1)

MAB_ACL_8021X = ACL(
    "mab_acl_8021x",
    0,
    {
        "rules": [
            {
                "eth_type": valve_of.ether.ETH_TYPE_IP,
                "eth_src": "01:02:03:04:05:06",
                "ip_proto": valve_of.inet.IPPROTO_UDP,
                "udp_src": 68,
                "udp_dst": 67,
                "actions": {"output": {"port": valve_of.ofp.OFPP_LOCAL}},
            }
        ]
    },
)
MAB_ACL_8021X.build({}, None, 1)
