def writeConfigDB(self, jDiff):
        '''
        Write the diff in Config DB.

        Parameters:
            jDiff (dict): config to push in config DB.

        Returns:
            void
        '''
        self.sysLog(doPrint=True, msg='Writing in Config DB')
        db_kwargs = dict(); data = dict()
        configdb = ConfigDBConnector(**db_kwargs)
        configdb.connect(False)
        deep_update(data, FormatConverter.to_deserialized(jDiff))
        self.sysLog(msg="Write in DB: {}".format(data))
        configdb.mod_config(FormatConverter.output_to_db(data))

        return
Beispiel #2
0
class AclLoader(object):

    ACL_TABLE = "ACL_TABLE"
    ACL_RULE = "ACL_RULE"
    ACL_TABLE_TYPE_MIRROR = "MIRROR"
    ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"
    CFG_MIRROR_SESSION_TABLE = "MIRROR_SESSION"
    STATE_MIRROR_SESSION_TABLE = "MIRROR_SESSION_TABLE"
    POLICER = "POLICER"
    SESSION_PREFIX = "everflow"
    SWITCH_CAPABILITY_TABLE = "SWITCH_CAPABILITY"
    ACL_ACTIONS_CAPABILITY_FIELD = "ACL_ACTIONS"
    ACL_ACTION_CAPABILITY_FIELD = "ACL_ACTION"

    min_priority = 1
    max_priority = 10000

    ethertype_map = {
        "ETHERTYPE_LLDP": 0x88CC,
        "ETHERTYPE_VLAN": 0x8100,
        "ETHERTYPE_ROCE": 0x8915,
        "ETHERTYPE_ARP": 0x0806,
        "ETHERTYPE_IPV4": 0x0800,
        "ETHERTYPE_IPV6": 0x86DD,
        "ETHERTYPE_MPLS": 0x8847
    }

    ip_protocol_map = {
        "IP_TCP": 6,
        "IP_ICMP": 1,
        "IP_UDP": 17,
        "IP_IGMP": 2,
        "IP_PIM": 103,
        "IP_RSVP": 46,
        "IP_GRE": 47,
        "IP_AUTH": 51,
        "IP_L2TP": 115
    }

    def __init__(self):
        self.yang_acl = None
        self.requested_session = None
        self.mirror_stage = None
        self.current_table = None
        self.tables_db_info = {}
        self.rules_db_info = {}
        self.rules_info = {}
        # Load global db config. This call is no-op in single npu platforms
        SonicDBConfig.load_sonic_global_db_config()
        self.sessions_db_info = {}
        self.configdb = ConfigDBConnector()
        self.configdb.connect()
        self.statedb = SonicV2Connector(host="127.0.0.1")
        self.statedb.connect(self.statedb.STATE_DB)

        # For multi-npu architecture we will have both global and per front asic namespace.
        # Global namespace will be used for Control plane ACL which are via IPTables.
        # Per ASIC namespace will be used for Data and Everflow ACL's.
        # Global Configdb will have all ACL information for both Ctrl and Data/Evereflow ACL's
        # and will be used as souurce of truth for ACL modification to config DB which will be done to both Global DB and
        # front asic namespace

        self.per_npu_configdb = {}

        # State DB are used for to get mirror Session monitor port.
        # For multi-npu platforms each asic namespace can have different monitor port
        # dependinding on which route to session destination ip. So for multi-npu
        # platforms we get state db for all front asic namespace in addition to

        self.per_npu_statedb = {}

        # Getting all front asic namespace and correspding config and state DB connector

        namespaces = device_info.get_all_namespaces()
        for front_asic_namespaces in namespaces['front_ns']:
            self.per_npu_configdb[front_asic_namespaces] = ConfigDBConnector(
                use_unix_socket_path=True, namespace=front_asic_namespaces)
            self.per_npu_configdb[front_asic_namespaces].connect()
            self.per_npu_statedb[front_asic_namespaces] = SonicV2Connector(
                use_unix_socket_path=True, namespace=front_asic_namespaces)
            self.per_npu_statedb[front_asic_namespaces].connect(
                self.per_npu_statedb[front_asic_namespaces].STATE_DB)

        self.read_tables_info()
        self.read_rules_info()
        self.read_sessions_info()
        self.read_policers_info()

    def read_tables_info(self):
        """
        Read ACL_TABLE table from configuration database
        :return:
        """
        self.tables_db_info = self.configdb.get_table(self.ACL_TABLE)

    def get_tables_db_info(self):
        return self.tables_db_info

    def read_rules_info(self):
        """
        Read ACL_RULE table from configuration database
        :return:
        """
        self.rules_db_info = self.configdb.get_table(self.ACL_RULE)

    def get_rules_db_info(self):
        return self.rules_db_info

    def read_policers_info(self):
        """
        Read POLICER table from configuration database
        :return:
        """

        # For multi-npu platforms we will read from any one of front asic namespace
        # config db as the information should be same across all config db
        if self.per_npu_configdb:
            namespace_configdb = list(self.per_npu_configdb.values())[0]
            self.policers_db_info = namespace_configdb.get_table(self.POLICER)
        else:
            self.policers_db_info = self.configdb.get_table(self.POLICER)

    def get_policers_db_info(self):
        return self.policers_db_info

    def read_sessions_info(self):
        """
        Read MIRROR_SESSION table from configuration database
        :return:
        """

        # For multi-npu platforms we will read from any one of front asic namespace
        # config db as the information should be same across all config db
        if self.per_npu_configdb:
            namespace_configdb = list(self.per_npu_configdb.values())[0]
            self.sessions_db_info = namespace_configdb.get_table(
                self.CFG_MIRROR_SESSION_TABLE)
        else:
            self.sessions_db_info = self.configdb.get_table(
                self.CFG_MIRROR_SESSION_TABLE)
        for key in self.sessions_db_info:
            if self.per_npu_statedb:
                # For multi-npu platforms we will read from all front asic name space
                # statedb as the monitor port will be differnt for each asic
                # and it's status also might be different (ideally should not happen)
                # We will store them as dict of 'asic' : value
                self.sessions_db_info[key]["status"] = {}
                self.sessions_db_info[key]["monitor_port"] = {}
                for namespace_key, namespace_statedb in self.per_npu_statedb.items(
                ):
                    state_db_info = namespace_statedb.get_all(
                        self.statedb.STATE_DB,
                        "{}|{}".format(self.STATE_MIRROR_SESSION_TABLE, key))
                    self.sessions_db_info[key]["status"][
                        namespace_key] = state_db_info.get(
                            "status", "inactive") if state_db_info else "error"
                    self.sessions_db_info[key][
                        "monitor_port"][namespace_key] = state_db_info.get(
                            "monitor_port", "") if state_db_info else ""
            else:
                state_db_info = self.statedb.get_all(
                    self.statedb.STATE_DB,
                    "{}|{}".format(self.STATE_MIRROR_SESSION_TABLE, key))
                self.sessions_db_info[key]["status"] = state_db_info.get(
                    "status", "inactive") if state_db_info else "error"
                self.sessions_db_info[key]["monitor_port"] = state_db_info.get(
                    "monitor_port", "") if state_db_info else ""

    def get_sessions_db_info(self):
        return self.sessions_db_info

    def get_session_name(self):
        """
        Get requested mirror session name or default session
        :return: Mirror session name
        """
        if self.requested_session:
            return self.requested_session

        for key in self.get_sessions_db_info():
            if key.startswith(self.SESSION_PREFIX):
                return key

        return None

    def set_table_name(self, table_name):
        """
        Set table name to restrict the table to be modified
        :param table_name: Table name
        :return:
        """
        if not self.is_table_valid(table_name):
            warning("Table \"%s\" not found" % table_name)

        self.current_table = table_name

    def set_session_name(self, session_name):
        """
        Set session name to be used in ACL rule action
        :param session_name: Mirror session name
        :return:
        """
        if session_name not in self.get_sessions_db_info():
            raise AclLoaderException("Session %s does not exist" %
                                     session_name)

        self.requested_session = session_name

    def set_mirror_stage(self, stage):
        """
        Set mirror stage to be used in ACL mirror rule action
        :param session_name: stage 'ingress'/'egress'
        :return:
        """
        self.mirror_stage = stage.upper()

    def set_max_priority(self, priority):
        """
        Set rules max priority
        :param priority: Rules max priority
        :return:
        """
        self.max_priority = int(priority)

    def is_table_valid(self, tname):
        return self.tables_db_info.get(tname)

    def is_table_mirror(self, tname):
        """
        Check if ACL table type is ACL_TABLE_TYPE_MIRROR or ACL_TABLE_TYPE_MIRRORV6
        :param tname: ACL table name
        :return: True if table type is MIRROR or MIRRORV6 else False
        """
        return self.tables_db_info[tname]['type'].upper().startswith(
            self.ACL_TABLE_TYPE_MIRROR)

    def is_table_control_plane(self, tname):
        """
        Check if ACL table type is ACL_TABLE_TYPE_CTRLPLANE
        :param tname: ACL table name
        :return: True if table type is ACL_TABLE_TYPE_CTRLPLANE else False
        """
        return self.tables_db_info[tname]['type'].upper(
        ) == self.ACL_TABLE_TYPE_CTRLPLANE

    @staticmethod
    def parse_acl_json(filename):
        yang_acl = pybindJSON.load(filename, openconfig_acl, "openconfig_acl")
        # Check pybindJSON parsing
        # pybindJSON.load will silently return an empty json object if input invalid
        with open(filename, 'r') as f:
            plain_json = json.load(f)
            if len(plain_json['acl']['acl-sets']['acl-set']) != len(
                    yang_acl.acl.acl_sets.acl_set):
                raise AclLoaderException("Invalid input file %s" % filename)
        return yang_acl

    def load_rules_from_file(self, filename):
        """
        Load file with ACL rules configuration in openconfig ACL format. Convert rules
        to Config DB schema.
        :param filename: File in openconfig ACL format
        :return:
        """
        self.yang_acl = AclLoader.parse_acl_json(filename)
        self.convert_rules()

    def convert_action(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.actions.config.forwarding_action == "ACCEPT":
            if self.is_table_control_plane(table_name):
                rule_props[AclAction.PACKET] = PacketAction.ACCEPT
            elif self.is_table_mirror(table_name):
                session_name = self.get_session_name()
                if not session_name:
                    raise AclLoaderException(
                        "Mirroring session does not exist")

                if self.mirror_stage == Stage.INGRESS:
                    mirror_action = AclAction.MIRROR_INGRESS
                elif self.mirror_stage == Stage.EGRESS:
                    mirror_action = AclAction.MIRROR_EGRESS
                else:
                    raise AclLoaderException(
                        "Invalid mirror stage passed {}".format(
                            self.mirror_stage))

                rule_props[mirror_action] = session_name
            else:
                rule_props[AclAction.PACKET] = PacketAction.FORWARD
        elif rule.actions.config.forwarding_action == "DROP":
            rule_props[AclAction.PACKET] = PacketAction.DROP
        elif rule.actions.config.forwarding_action == "REJECT":
            rule_props[AclAction.PACKET] = PacketAction.DROP
        else:
            raise AclLoaderException(
                "Unknown rule action {} in table {}, rule {}".format(
                    rule.actions.config.forwarding_action, table_name,
                    rule_idx))

        if not self.validate_actions(table_name, rule_props):
            raise AclLoaderException(
                "Rule action {} is not supported in table {}, rule {}".format(
                    rule.actions.config.forwarding_action, table_name,
                    rule_idx))

        return rule_props

    def validate_actions(self, table_name, action_props):
        if self.is_table_control_plane(table_name):
            return True

        action_count = len(action_props)

        if table_name not in self.tables_db_info:
            raise AclLoaderException(
                "Table {} does not exist".format(table_name))

        stage = self.tables_db_info[table_name].get("stage", Stage.INGRESS)

        # check if per npu state db is there then read using first state db
        # else read from global statedb
        if self.per_npu_statedb:
            # For multi-npu we will read using anyone statedb connector for front asic namespace.
            # Same information should be there in all state DB's
            # as it is static information about switch capability
            namespace_statedb = list(self.per_npu_statedb.values())[0]
            capability = namespace_statedb.get_all(
                self.statedb.STATE_DB,
                "{}|switch".format(self.SWITCH_CAPABILITY_TABLE))
        else:
            capability = self.statedb.get_all(
                self.statedb.STATE_DB,
                "{}|switch".format(self.SWITCH_CAPABILITY_TABLE))
        for action_key in dict(action_props):
            key = "{}|{}".format(self.ACL_ACTIONS_CAPABILITY_FIELD,
                                 stage.upper())
            if key not in capability:
                del action_props[action_key]
                continue

            values = capability[key].split(",")
            if action_key.upper() not in values:
                del action_props[action_key]
                continue

            if action_key == AclAction.PACKET:
                # Check if action_value is supported
                action_value = action_props[action_key]
                key = "{}|{}".format(self.ACL_ACTION_CAPABILITY_FIELD,
                                     action_key.upper())
                if key not in capability:
                    del action_props[action_key]
                    continue

                if action_value not in capability[key]:
                    del action_props[action_key]
                    continue

        return action_count == len(action_props)

    def convert_l2(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.l2.config.ethertype:
            if rule.l2.config.ethertype in self.ethertype_map:
                rule_props["ETHER_TYPE"] = self.ethertype_map[
                    rule.l2.config.ethertype]
            else:
                try:
                    rule_props["ETHER_TYPE"] = int(rule.l2.config.ethertype)
                except:
                    raise AclLoaderException(
                        "Failed to convert ethertype %s table %s rule %s" %
                        (rule.l2.config.ethertype, table_name, rule_idx))

        return rule_props

    def convert_ip(self, table_name, rule_idx, rule):
        rule_props = {}

        # FIXME: 0 is a valid protocol number, but openconfig seems to use it as a default value,
        # so there isn't currently a good way to check if the user defined proto=0 or not.
        if rule.ip.config.protocol:
            if rule.ip.config.protocol in self.ip_protocol_map:
                rule_props["IP_PROTOCOL"] = self.ip_protocol_map[
                    rule.ip.config.protocol]
            else:
                try:
                    int(rule.ip.config.protocol)
                except:
                    raise AclLoaderException(
                        "Unknown rule protocol %s in table %s, rule %d!" %
                        (rule.ip.config.protocol, table_name, rule_idx))

                rule_props["IP_PROTOCOL"] = rule.ip.config.protocol

        if rule.ip.config.source_ip_address:
            source_ip_address = rule.ip.config.source_ip_address.encode(
                "ascii")
            if ipaddress.ip_network(source_ip_address).version == 4:
                rule_props["SRC_IP"] = source_ip_address
            else:
                rule_props["SRC_IPV6"] = source_ip_address

        if rule.ip.config.destination_ip_address:
            destination_ip_address = rule.ip.config.destination_ip_address.encode(
                "ascii")
            if ipaddress.ip_network(destination_ip_address).version == 4:
                rule_props["DST_IP"] = destination_ip_address
            else:
                rule_props["DST_IPV6"] = destination_ip_address

        # NOTE: DSCP is available only for MIRROR table
        if self.is_table_mirror(table_name):
            if rule.ip.config.dscp:
                rule_props["DSCP"] = rule.ip.config.dscp

        return rule_props

    def convert_port(self, port):
        """
        Convert port field format from openconfig ACL to Config DB schema
        :param port: String, ACL port number or range in openconfig format
        :return: Tuple, first value is converted port string,
            second value is boolean, True if value is a port range, False
            if it is a single port value
        """
        # OpenConfig port range is of the format "####..####", whereas
        # Config DB format is "####-####"
        if ".." in port:
            return port.replace("..", "-"), True
        else:
            return port, False

    def convert_transport(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.transport.config.source_port:
            port, is_range = self.convert_port(
                str(rule.transport.config.source_port))
            rule_props[
                "L4_SRC_PORT_RANGE" if is_range else "L4_SRC_PORT"] = port
        if rule.transport.config.destination_port:
            port, is_range = self.convert_port(
                str(rule.transport.config.destination_port))
            rule_props[
                "L4_DST_PORT_RANGE" if is_range else "L4_DST_PORT"] = port

        tcp_flags = 0x00

        for flag in rule.transport.config.tcp_flags:
            if flag == "TCP_FIN":
                tcp_flags |= 0x01
            if flag == "TCP_SYN":
                tcp_flags |= 0x02
            if flag == "TCP_RST":
                tcp_flags |= 0x04
            if flag == "TCP_PSH":
                tcp_flags |= 0x08
            if flag == "TCP_ACK":
                tcp_flags |= 0x10
            if flag == "TCP_URG":
                tcp_flags |= 0x20
            if flag == "TCP_ECE":
                tcp_flags |= 0x40
            if flag == "TCP_CWR":
                tcp_flags |= 0x80

        if tcp_flags:
            rule_props["TCP_FLAGS"] = '0x{:02x}/0x{:02x}'.format(
                tcp_flags, tcp_flags)

        return rule_props

    def convert_input_interface(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.input_interface.interface_ref.config.interface:
            rule_props[
                "IN_PORTS"] = rule.input_interface.interface_ref.config.interface

        return rule_props

    def convert_rule_to_db_schema(self, table_name, rule):
        """
        Convert rules format from openconfig ACL to Config DB schema
        :param table_name: ACL table name to which rule belong
        :param rule: ACL rule in openconfig format
        :return: dict with Config DB schema
        """
        rule_idx = int(rule.config.sequence_id)
        rule_props = {}
        rule_data = {(table_name, "RULE_" + str(rule_idx)): rule_props}

        rule_props["PRIORITY"] = str(self.max_priority - rule_idx)

        deep_update(rule_props, self.convert_action(table_name, rule_idx,
                                                    rule))
        deep_update(rule_props, self.convert_l2(table_name, rule_idx, rule))
        deep_update(rule_props, self.convert_ip(table_name, rule_idx, rule))
        deep_update(rule_props,
                    self.convert_transport(table_name, rule_idx, rule))
        deep_update(rule_props,
                    self.convert_input_interface(table_name, rule_idx, rule))

        return rule_data

    def deny_rule(self, table_name):
        """
        Create default deny rule in Config DB format
        :param table_name: ACL table name to which rule belong
        :return: dict with Config DB schema
        """
        rule_props = {}
        rule_data = {(table_name, "DEFAULT_RULE"): rule_props}
        rule_props["PRIORITY"] = str(self.min_priority)
        rule_props["PACKET_ACTION"] = "DROP"
        if 'v6' in table_name.lower():
            rule_props["ETHER_TYPE"] = str(
                self.ethertype_map["ETHERTYPE_IPV6"])
        else:
            rule_props["ETHER_TYPE"] = str(
                self.ethertype_map["ETHERTYPE_IPV4"])
        return rule_data

    def convert_rules(self):
        """
        Convert rules in openconfig ACL format to Config DB schema
        :return:
        """
        for acl_set_name in self.yang_acl.acl.acl_sets.acl_set:
            table_name = acl_set_name.replace(" ", "_").replace(
                "-", "_").upper().encode('ascii')
            acl_set = self.yang_acl.acl.acl_sets.acl_set[acl_set_name]

            if not self.is_table_valid(table_name):
                warning("%s table does not exist" % (table_name))
                continue

            if self.current_table is not None and self.current_table != table_name:
                continue

            for acl_entry_name in acl_set.acl_entries.acl_entry:
                acl_entry = acl_set.acl_entries.acl_entry[acl_entry_name]
                try:
                    rule = self.convert_rule_to_db_schema(
                        table_name, acl_entry)
                    deep_update(self.rules_info, rule)
                except AclLoaderException as ex:
                    error("Error processing rule %s: %s. Skipped." %
                          (acl_entry_name, ex))

            if not self.is_table_mirror(table_name):
                deep_update(self.rules_info, self.deny_rule(table_name))

    def full_update(self):
        """
        Perform full update of ACL rules configuration. All existing rules
        will be removed. New rules loaded from file will be installed. If
        the current_table is not empty, only rules within that table will
        be removed and new rules in that table will be installed.
        :return:
        """
        for key in self.rules_db_info:
            if self.current_table is None or self.current_table == key[0]:
                self.configdb.mod_entry(self.ACL_RULE, key, None)
                # Program for per front asic namespace also if present
                for namespace_configdb in self.per_npu_configdb.values():
                    namespace_configdb.mod_entry(self.ACL_RULE, key, None)

        self.configdb.mod_config({self.ACL_RULE: self.rules_info})
        # Program for per front asic namespace also if present
        for namespace_configdb in self.per_npu_configdb.values():
            namespace_configdb.mod_config({self.ACL_RULE: self.rules_info})

    def incremental_update(self):
        """
        Perform incremental ACL rules configuration update. Get existing rules from
        Config DB. Compare with rules specified in file and perform corresponding
        modifications.
        :return:
        """

        # TODO: Until we test ASIC behavior, we cannot assume that we can insert
        # dataplane ACLs and shift existing ACLs. Therefore, we perform a full
        # update on dataplane ACLs, and only perform an incremental update on
        # control plane ACLs.

        new_rules = set(self.rules_info.keys())
        new_dataplane_rules = set()
        new_controlplane_rules = set()
        current_rules = set(self.rules_db_info.keys())
        current_dataplane_rules = set()
        current_controlplane_rules = set()

        for key in new_rules:
            table_name = key[0]
            if self.tables_db_info[table_name]['type'].upper(
            ) == self.ACL_TABLE_TYPE_CTRLPLANE:
                new_controlplane_rules.add(key)
            else:
                new_dataplane_rules.add(key)

        for key in current_rules:
            table_name = key[0]
            if self.tables_db_info[table_name]['type'].upper(
            ) == self.ACL_TABLE_TYPE_CTRLPLANE:
                current_controlplane_rules.add(key)
            else:
                current_dataplane_rules.add(key)

        # Remove all existing dataplane rules
        for key in current_dataplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, None)
            # Program for per-asic namespace also if present
            for namespace_configdb in self.per_npu_configdb.values():
                namespace_configdb.mod_entry(self.ACL_RULE, key, None)

        # Add all new dataplane rules
        for key in new_dataplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])
            # Program for per-asic namespace corresponding to front asic also if present.
            for namespace_configdb in self.per_npu_configdb.values():
                namespace_configdb.mod_entry(self.ACL_RULE, key,
                                             self.rules_info[key])

        added_controlplane_rules = new_controlplane_rules.difference(
            current_controlplane_rules)
        removed_controlplane_rules = current_controlplane_rules.difference(
            new_controlplane_rules)
        existing_controlplane_rules = new_rules.intersection(
            current_controlplane_rules)

        for key in added_controlplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])
            # Program for per-asic namespace corresponding to front asic also if present.
            # For control plane ACL it's not needed but to keep all db in sync program everywhere
            for namespace_configdb in self.per_npu_configdb.values():
                namespace_configdb.mod_entry(self.ACL_RULE, key,
                                             self.rules_info[key])

        for key in removed_controlplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, None)
            # Program for per-asic namespace corresponding to front asic also if present.
            # For control plane ACL it's not needed but to keep all db in sync program everywhere
            for namespace_configdb in self.per_npu_configdb.values():
                namespace_configdb.mod_entry(self.ACL_RULE, key, None)

        for key in existing_controlplane_rules:
            if cmp(self.rules_info[key], self.rules_db_info[key]) != 0:
                self.configdb.set_entry(self.ACL_RULE, key,
                                        self.rules_info[key])
                # Program for per-asic namespace corresponding to front asic also if present.
                # For control plane ACL it's not needed but to keep all db in sync program everywhere
                for namespace_configdb in self.per_npu_configdb.values():
                    namespace_configdb.set_entry(self.ACL_RULE, key,
                                                 self.rules_info[key])

    def delete(self, table=None, rule=None):
        """
        :param table:
        :param rule:
        :return:
        """
        for key in self.rules_db_info:
            if not table or table == key[0]:
                if not rule or rule == key[1]:
                    self.configdb.set_entry(self.ACL_RULE, key, None)
                    # Program for per-asic namespace corresponding to front asic also if present.
                    for namespace_configdb in self.per_npu_configdb.values():
                        namespace_configdb.set_entry(self.ACL_RULE, key, None)

    def show_table(self, table_name):
        """
        Show ACL table configuration.
        :param table_name: Optional. ACL table name. Filter tables by specified name.
        :return:
        """
        header = ("Name", "Type", "Binding", "Description", "Stage")

        data = []
        for key, val in self.get_tables_db_info().items():
            if table_name and key != table_name:
                continue

            stage = val.get("stage", Stage.INGRESS).lower()

            if val["type"] == AclLoader.ACL_TABLE_TYPE_CTRLPLANE:
                services = natsorted(val["services"])
                data.append(
                    [key, val["type"], services[0], val["policy_desc"], stage])

                if len(services) > 1:
                    for service in services[1:]:
                        data.append(["", "", service, "", ""])
            else:
                if not val["ports"]:
                    data.append(
                        [key, val["type"], "", val["policy_desc"], stage])
                else:
                    ports = natsorted(val["ports"])
                    data.append([
                        key, val["type"], ports[0], val["policy_desc"], stage
                    ])

                    if len(ports) > 1:
                        for port in ports[1:]:
                            data.append(["", "", port, "", ""])

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))

    def show_session(self, session_name):
        """
        Show mirror session configuration.
        :param session_name: Optional. Mirror session name. Filter sessions by specified name.
        :return:
        """
        erspan_header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP",
                         "TTL", "Queue", "Policer", "Monitor Port", "SRC Port",
                         "Direction")
        span_header = ("Name", "Status", "DST Port", "SRC Port", "Direction",
                       "Queue", "Policer")

        erspan_data = []
        span_data = []
        for key, val in self.get_sessions_db_info().items():
            if session_name and key != session_name:
                continue

            if val.get("type") == "SPAN":
                span_data.append([
                    key,
                    val.get("status", ""),
                    val.get("dst_port", ""),
                    val.get("src_port", ""),
                    val.get("direction", "").lower(),
                    val.get("queue", ""),
                    val.get("policer", "")
                ])
            else:
                erspan_data.append([
                    key,
                    val.get("status", ""),
                    val.get("src_ip", ""),
                    val.get("dst_ip", ""),
                    val.get("gre_type", ""),
                    val.get("dscp", ""),
                    val.get("ttl", ""),
                    val.get("queue", ""),
                    val.get("policer", ""),
                    val.get("monitor_port", ""),
                    val.get("src_port", ""),
                    val.get("direction", "").lower()
                ])

        print("ERSPAN Sessions")
        print(
            tabulate.tabulate(erspan_data,
                              headers=erspan_header,
                              tablefmt="simple",
                              missingval=""))
        print("\nSPAN Sessions")
        print(
            tabulate.tabulate(span_data,
                              headers=span_header,
                              tablefmt="simple",
                              missingval=""))

    def show_policer(self, policer_name):
        """
        Show policer configuration.
        :param policer_name: Optional. Policer name. Filter policers by specified name.
        :return:
        """
        header = ("Name", "Type", "Mode", "CIR", "CBS")

        data = []
        for key, val in self.get_policers_db_info().items():
            if policer_name and key != policer_name:
                continue

            data.append([
                key, val["meter_type"], val["mode"],
                val.get("cir", ""),
                val.get("cbs", "")
            ])

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))

    def show_rule(self, table_name, rule_id):
        """
        Show ACL rules configuration.
        :param table_name: Optional. ACL table name. Filter rules by specified table name.
        :param rule_id: Optional. ACL rule name. Filter rule by specified rule name.
        :return:
        """
        header = ("Table", "Rule", "Priority", "Action", "Match")

        def pop_priority(val):
            priority = "N/A"
            for key in dict(val):
                if (key.upper() == "PRIORITY"):
                    priority = val.pop(key)
                    return priority
            return priority

        def pop_action(val):
            action = ""

            for key in dict(val):
                key = key.upper()
                if key == AclAction.PACKET:
                    action = val.pop(key)
                elif key == AclAction.REDIRECT:
                    action = "REDIRECT: {}".format(val.pop(key))
                elif key in (AclAction.MIRROR, AclAction.MIRROR_INGRESS):
                    action = "MIRROR INGRESS: {}".format(val.pop(key))
                elif key == AclAction.MIRROR_EGRESS:
                    action = "MIRROR EGRESS: {}".format(val.pop(key))
                else:
                    continue

            return action

        def pop_matches(val):
            matches = list(sorted(["%s: %s" % (k, val[k]) for k in val]))
            if len(matches) == 0:
                matches.append("N/A")
            return matches

        raw_data = []
        for (tname, rid), val in self.get_rules_db_info().items():

            if table_name and table_name != tname:
                continue

            if rule_id and rule_id != rid:
                continue

            priority = pop_priority(val)
            action = pop_action(val)
            matches = pop_matches(val)

            rule_data = [[tname, rid, priority, action, matches[0]]]
            if len(matches) > 1:
                for m in matches[1:]:
                    rule_data.append(["", "", "", "", m])

            raw_data.append([priority, rule_data])

        def cmp_rules(a, b):
            return cmp(a[0], b[0])

        raw_data.sort(cmp_rules)
        raw_data.reverse()

        data = []
        for _, d in raw_data:
            data += d

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))
Beispiel #3
0
class AclLoader(object):

    ACL_TABLE = "ACL_TABLE"
    ACL_RULE = "ACL_RULE"
    ACL_TABLE_TYPE_MIRROR = "MIRROR"
    ACL_TABLE_TYPE_CTRLPLANE = "CTRLPLANE"
    MIRROR_SESSION = "MIRROR_SESSION"
    SESSION_PREFIX = "everflow"

    min_priority = 1
    max_priority = 10000

    ethertype_map = {
        "ETHERTYPE_LLDP": 0x88CC,
        "ETHERTYPE_VLAN": 0x8100,
        "ETHERTYPE_ROCE": 0x8915,
        "ETHERTYPE_ARP": 0x0806,
        "ETHERTYPE_IPV4": 0x0800,
        "ETHERTYPE_IPV6": 0x86DD,
        "ETHERTYPE_MPLS": 0x8847
    }

    ip_protocol_map = {
        "IP_TCP": 6,
        "IP_ICMP": 1,
        "IP_UDP": 17,
        "IP_IGMP": 2,
        "IP_PIM": 103,
        "IP_RSVP": 46,
        "IP_GRE": 47,
        "IP_AUTH": 51,
        "IP_L2TP": 115
    }

    def __init__(self):
        self.yang_acl = None
        self.requested_session = None
        self.current_table = None
        self.tables_db_info = {}
        self.rules_db_info = {}
        self.rules_info = {}
        self.sessions_db_info = {}
        self.configdb = ConfigDBConnector()
        self.configdb.connect()
        self.statedb = SonicV2Connector(host="127.0.0.1")
        self.statedb.connect(self.statedb.STATE_DB)

        self.read_tables_info()
        self.read_rules_info()
        self.read_sessions_info()

    def read_tables_info(self):
        """
        Read ACL tables information from Config DB
        :return:
        """
        self.tables_db_info = self.configdb.get_table(self.ACL_TABLE)

    def get_tables_db_info(self):
        return self.tables_db_info

    def read_rules_info(self):
        """
        Read rules information from Config DB
        :return:
        """
        self.rules_db_info = self.configdb.get_table(self.ACL_RULE)

    def get_rules_db_info(self):
        return self.rules_db_info

    def read_sessions_info(self):
        """
        Read ACL tables information from Config DB
        :return:
        """
        self.sessions_db_info = self.configdb.get_table(self.MIRROR_SESSION)
        for key in self.sessions_db_info.keys():
            state_db_info = self.statedb.get_all(
                self.statedb.STATE_DB, "{}|{}".format(self.MIRROR_SESSION,
                                                      key))
            if state_db_info:
                status = state_db_info.get("status", "inactive")
            else:
                status = "error"
            self.sessions_db_info[key]["status"] = status

    def get_sessions_db_info(self):
        """
        Read mirror session information from Config DB
        :return:
        """
        return self.sessions_db_info

    def get_session_name(self):
        """
        Read mirror session name from Config DB
        :return: Mirror session name
        """
        if self.requested_session:
            return self.requested_session

        for key in self.get_sessions_db_info():
            if key.startswith(self.SESSION_PREFIX):
                return key

        return None

    def set_table_name(self, table_name):
        """
        Set table name to restrict the table to be modified
        :param table_name: Table name
        :return:
        """
        self.current_table = table_name

    def set_session_name(self, session_name):
        """
        Set session name to be used in ACL rule action
        :param session_name: Mirror session name
        :return:
        """
        if session_name not in self.get_sessions_db_info():
            raise AclLoaderException("Session %s does not exist" %
                                     session_name)

        self.requested_session = session_name

    def set_max_priority(self, priority):
        """
        Set rules max priority
        :param priority: Rules max priority
        :return:
        """
        self.max_priority = int(priority)

    def is_table_valid(self, tname):
        return self.tables_db_info.get(tname)

    def is_table_mirror(self, tname):
        """
        Check if ACL table type is ACL_TABLE_TYPE_MIRROR
        :param tname: ACL table name
        :return: True if table type is ACL_TABLE_TYPE_MIRROR else False
        """
        return self.tables_db_info[tname]['type'].upper(
        ) == self.ACL_TABLE_TYPE_MIRROR

    def is_table_control_plane(self, tname):
        """
        Check if ACL table type is ACL_TABLE_TYPE_CTRLPLANE
        :param tname: ACL table name
        :return: True if table type is ACL_TABLE_TYPE_CTRLPLANE else False
        """
        return self.tables_db_info[tname]['type'].upper(
        ) == self.ACL_TABLE_TYPE_CTRLPLANE

    @staticmethod
    def parse_acl_json(filename):
        yang_acl = pybindJSON.load(filename, openconfig_acl, "openconfig_acl")
        # Check pybindJSON parsing
        # pybindJSON.load will silently return an empty json object if input invalid
        with open(filename, 'r') as f:
            plain_json = json.load(f)
            if len(plain_json['acl']['acl-sets']['acl-set']) != len(
                    yang_acl.acl.acl_sets.acl_set):
                raise AclLoaderException("Invalid input file %s" % filename)
        return yang_acl

    def load_rules_from_file(self, filename):
        """
        Load file with ACL rules configuration in openconfig ACL format. Convert rules
        to Config DB schema.
        :param filename: File in openconfig ACL format
        :return:
        """
        self.yang_acl = AclLoader.parse_acl_json(filename)
        self.convert_rules()

    def convert_action(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.actions.config.forwarding_action == "ACCEPT":
            if self.is_table_control_plane(table_name):
                rule_props["PACKET_ACTION"] = "ACCEPT"
            elif self.is_table_mirror(table_name):
                session_name = self.get_session_name()
                if not session_name:
                    raise AclLoaderException(
                        "Mirroring session does not exist")

                rule_props["MIRROR_ACTION"] = session_name
            else:
                rule_props["PACKET_ACTION"] = "FORWARD"
        elif rule.actions.config.forwarding_action == "DROP":
            rule_props["PACKET_ACTION"] = "DROP"
        elif rule.actions.config.forwarding_action == "REJECT":
            rule_props["PACKET_ACTION"] = "DROP"
        else:
            raise AclLoaderException(
                "Unknown rule action %s in table %s, rule %d" %
                (rule.actions.config.forwarding_action, table_name, rule_idx))

        return rule_props

    def convert_l2(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.l2.config.ethertype:
            if rule.l2.config.ethertype in self.ethertype_map:
                rule_props["ETHER_TYPE"] = self.ethertype_map[
                    rule.l2.config.ethertype]
            else:
                try:
                    rule_props["ETHER_TYPE"] = int(rule.l2.config.ethertype)
                except:
                    raise AclLoaderException(
                        "Failed to convert ethertype %s table %s rule %s" %
                        (rule.l2.config.ethertype, table_name, rule_idx))

        return rule_props

    def convert_ip(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.ip.config.protocol:
            if self.ip_protocol_map.has_key(rule.ip.config.protocol):
                rule_props["IP_PROTOCOL"] = self.ip_protocol_map[
                    rule.ip.config.protocol]
            else:
                try:
                    int(rule.ip.config.protocol)
                except:
                    raise AclLoaderException(
                        "Unknown rule protocol %s in table %s, rule %d!" %
                        (rule.ip.config.protocol, table_name, rule_idx))

                rule_props["IP_PROTOCOL"] = rule.ip.config.protocol

        if rule.ip.config.source_ip_address:
            source_ip_address = rule.ip.config.source_ip_address.encode(
                "ascii")
            if ipaddr.IPNetwork(source_ip_address).version == 4:
                rule_props["SRC_IP"] = source_ip_address
            else:
                rule_props["SRC_IPV6"] = source_ip_address

        if rule.ip.config.destination_ip_address:
            destination_ip_address = rule.ip.config.destination_ip_address.encode(
                "ascii")
            if ipaddr.IPNetwork(destination_ip_address).version == 4:
                rule_props["DST_IP"] = destination_ip_address
            else:
                rule_props["DST_IPV6"] = destination_ip_address

        # NOTE: DSCP is available only for MIRROR table
        if self.is_table_mirror(table_name):
            if rule.ip.config.dscp:
                rule_props["DSCP"] = rule.ip.config.dscp

        return rule_props

    def convert_port(self, port):
        """
        Convert port field format from openconfig ACL to Config DB schema
        :param port: String, ACL port number or range in openconfig format
        :return: Tuple, first value is converted port string,
            second value is boolean, True if value is a port range, False
            if it is a single port value
        """
        # OpenConfig port range is of the format "####..####", whereas
        # Config DB format is "####-####"
        if ".." in port:
            return port.replace("..", "-"), True
        else:
            return port, False

    def convert_transport(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.transport.config.source_port:
            port, is_range = self.convert_port(
                str(rule.transport.config.source_port))
            rule_props[
                "L4_SRC_PORT_RANGE" if is_range else "L4_SRC_PORT"] = port
        if rule.transport.config.destination_port:
            port, is_range = self.convert_port(
                str(rule.transport.config.destination_port))
            rule_props[
                "L4_DST_PORT_RANGE" if is_range else "L4_DST_PORT"] = port

        tcp_flags = 0x00

        for flag in rule.transport.config.tcp_flags:
            if flag == "TCP_FIN":
                tcp_flags |= 0x01
            if flag == "TCP_SYN":
                tcp_flags |= 0x02
            if flag == "TCP_RST":
                tcp_flags |= 0x04
            if flag == "TCP_PSH":
                tcp_flags |= 0x08
            if flag == "TCP_ACK":
                tcp_flags |= 0x10
            if flag == "TCP_URG":
                tcp_flags |= 0x20
            if flag == "TCP_ECE":
                tcp_flags |= 0x40
            if flag == "TCP_CWR":
                tcp_flags |= 0x80

        if tcp_flags:
            rule_props["TCP_FLAGS"] = '0x{:02x}/0x{:02x}'.format(
                tcp_flags, tcp_flags)

        return rule_props

    def convert_input_interface(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.input_interface.interface_ref.config.interface:
            rule_props[
                "IN_PORTS"] = rule.input_interface.interface_ref.config.interface

        return rule_props

    def convert_rule_to_db_schema(self, table_name, rule):
        """
        Convert rules format from openconfig ACL to Config DB schema
        :param table_name: ACL table name to which rule belong
        :param rule: ACL rule in openconfig format
        :return: dict with Config DB schema
        """
        rule_idx = int(rule.config.sequence_id)
        rule_props = {}
        rule_data = {(table_name, "RULE_" + str(rule_idx)): rule_props}

        rule_props["PRIORITY"] = str(self.max_priority - rule_idx)

        deep_update(rule_props, self.convert_action(table_name, rule_idx,
                                                    rule))
        deep_update(rule_props, self.convert_l2(table_name, rule_idx, rule))
        deep_update(rule_props, self.convert_ip(table_name, rule_idx, rule))
        deep_update(rule_props,
                    self.convert_transport(table_name, rule_idx, rule))
        deep_update(rule_props,
                    self.convert_input_interface(table_name, rule_idx, rule))

        return rule_data

    def deny_rule(self, table_name):
        """
        Create default deny rule in Config DB format
        :param table_name: ACL table name to which rule belong
        :return: dict with Config DB schema
        """
        rule_props = {}
        rule_data = {(table_name, "DEFAULT_RULE"): rule_props}
        rule_props["PRIORITY"] = str(self.min_priority)
        rule_props["ETHER_TYPE"] = str(self.ethertype_map["ETHERTYPE_IPV4"])
        rule_props["PACKET_ACTION"] = "DROP"
        return rule_data

    def convert_rules(self):
        """
        Convert rules in openconfig ACL format to Config DB schema
        :return:
        """
        for acl_set_name in self.yang_acl.acl.acl_sets.acl_set:
            table_name = acl_set_name.replace(" ", "_").replace(
                "-", "_").upper().encode('ascii')
            acl_set = self.yang_acl.acl.acl_sets.acl_set[acl_set_name]

            if not self.is_table_valid(table_name):
                warning("%s table does not exist" % (table_name))
                continue

            if self.current_table is not None and self.current_table != table_name:
                continue

            for acl_entry_name in acl_set.acl_entries.acl_entry:
                acl_entry = acl_set.acl_entries.acl_entry[acl_entry_name]
                try:
                    rule = self.convert_rule_to_db_schema(
                        table_name, acl_entry)
                    deep_update(self.rules_info, rule)
                except AclLoaderException as ex:
                    error("Error processing rule %s: %s. Skipped." %
                          (acl_entry_name, ex))

            if not self.is_table_mirror(table_name):
                deep_update(self.rules_info, self.deny_rule(table_name))

    def full_update(self):
        """
        Perform full update of ACL rules configuration. All existing rules
        will be removed. New rules loaded from file will be installed. If
        the current_table is not empty, only rules within that table will
        be removed and new rules in that table will be installed.
        :return:
        """
        for key in self.rules_db_info.keys():
            if self.current_table is None or self.current_table == key[0]:
                self.configdb.mod_entry(self.ACL_RULE, key, None)

        self.configdb.mod_config({self.ACL_RULE: self.rules_info})

    def incremental_update(self):
        """
        Perform incremental ACL rules configuration update. Get existing rules from
        Config DB. Compare with rules specified in file and perform corresponding
        modifications.
        :return:
        """

        # TODO: Until we test ASIC behavior, we cannot assume that we can insert
        # dataplane ACLs and shift existing ACLs. Therefore, we perform a full
        # update on dataplane ACLs, and only perform an incremental update on
        # control plane ACLs.

        new_rules = set(self.rules_info.iterkeys())
        new_dataplane_rules = set()
        new_controlplane_rules = set()
        current_rules = set(self.rules_db_info.iterkeys())
        current_dataplane_rules = set()
        current_controlplane_rules = set()

        for key in new_rules:
            table_name = key[0]
            if self.tables_db_info[table_name]['type'].upper(
            ) == self.ACL_TABLE_TYPE_CTRLPLANE:
                new_controlplane_rules.add(key)
            else:
                new_dataplane_rules.add(key)

        for key in current_rules:
            table_name = key[0]
            if self.tables_db_info[table_name]['type'].upper(
            ) == self.ACL_TABLE_TYPE_CTRLPLANE:
                current_controlplane_rules.add(key)
            else:
                current_dataplane_rules.add(key)

        # Remove all existing dataplane rules
        for key in current_dataplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, None)

        # Add all new dataplane rules
        for key in new_dataplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])

        added_controlplane_rules = new_controlplane_rules.difference(
            current_controlplane_rules)
        removed_controlplane_rules = current_controlplane_rules.difference(
            new_controlplane_rules)
        existing_controlplane_rules = new_rules.intersection(
            current_controlplane_rules)

        for key in added_controlplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])

        for key in removed_controlplane_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, None)

        for key in existing_controlplane_rules:
            if cmp(self.rules_info[key], self.rules_db_info[key]) != 0:
                self.configdb.set_entry(self.ACL_RULE, key,
                                        self.rules_info[key])

    def delete(self, table=None, rule=None):
        """
        :param table:
        :param rule:
        :return:
        """
        for key in self.rules_db_info.iterkeys():
            if not table or table == key[0]:
                if not rule or rule == key[1]:
                    self.configdb.set_entry(self.ACL_RULE, key, None)

    def show_table(self, table_name):
        """
        Show ACL table configuration.
        :param table_name: Optional. ACL table name. Filter tables by specified name.
        :return:
        """
        header = ("Name", "Type", "Binding", "Description")

        data = []
        for key, val in self.get_tables_db_info().iteritems():
            if table_name and key != table_name:
                continue

            if val["type"] == AclLoader.ACL_TABLE_TYPE_CTRLPLANE:
                services = natsorted(val["services"])
                data.append(
                    [key, val["type"], services[0], val["policy_desc"]])

                if len(services) > 1:
                    for service in services[1:]:
                        data.append(["", "", service, ""])
            else:
                if not val["ports"]:
                    data.append([key, val["type"], "", val["policy_desc"]])
                else:
                    ports = natsorted(val["ports"])
                    data.append(
                        [key, val["type"], ports[0], val["policy_desc"]])

                    if len(ports) > 1:
                        for port in ports[1:]:
                            data.append(["", "", port, ""])

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))

    def show_session(self, session_name):
        """
        Show mirror session configuration.
        :param session_name: Optional. Mirror session name. Filter sessions by specified name.
        :return:
        """
        header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP", "TTL",
                  "Queue")

        data = []
        for key, val in self.get_sessions_db_info().iteritems():
            if session_name and key != session_name:
                continue

            data.append([
                key, val["status"], val["src_ip"], val["dst_ip"],
                val.get("gre_type", ""),
                val.get("dscp", ""),
                val.get("ttl", ""),
                val.get("queue", "")
            ])

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))

    def show_rule(self, table_name, rule_id):
        """
        Show ACL rules configuration.
        :param table_name: Optional. ACL table name. Filter rules by specified table name.
        :param rule_id: Optional. ACL rule name. Filter rule by specified rule name.
        :return:
        """
        header = ("Table", "Rule", "Priority", "Action", "Match")

        ignore_list = ["PRIORITY", "PACKET_ACTION", "MIRROR_ACTION"]

        raw_data = []
        for (tname, rid), val in self.get_rules_db_info().iteritems():

            if table_name and table_name != tname:
                continue

            if rule_id and rule_id != rid:
                continue

            priority = val["PRIORITY"]

            action = ""
            if "PACKET_ACTION" in val:
                action = val["PACKET_ACTION"]
            elif "MIRROR_ACTION" in val:
                action = "MIRROR: %s" % val["MIRROR_ACTION"]
            else:
                continue

            matches = [
                "%s: %s" % (k, v) for k, v in val.iteritems()
                if k not in ignore_list
            ]

            matches.sort()

            rule_data = [[tname, rid, priority, action, matches[0]]]
            if len(matches) > 1:
                for m in matches[1:]:
                    rule_data.append(["", "", "", "", m])

            raw_data.append([priority, rule_data])

        def cmp_rules(a, b):
            return cmp(a[0], b[0])

        raw_data.sort(cmp_rules)
        raw_data.reverse()

        data = []
        for _, d in raw_data:
            data += d

        print(
            tabulate.tabulate(data,
                              headers=header,
                              tablefmt="simple",
                              missingval=""))
Beispiel #4
0
class AclLoader(object):

    ACL_TABLE = "ACL_TABLE"
    ACL_RULE = "ACL_RULE"
    MIRROR_SESSION = "MIRROR_SESSION"
    SESSION_PREFIX = "everflow"

    min_priority = 1
    max_priority = 10000

    ethertype_map = {
        "ETHERTYPE_LLDP": 0x88CC,
        "ETHERTYPE_VLAN": 0x8100,
        "ETHERTYPE_ROCE": 0x8915,
        "ETHERTYPE_ARP": 0x0806,
        "ETHERTYPE_IPV4": 0x0800,
        "ETHERTYPE_IPV6": 0x86DD,
        "ETHERTYPE_MPLS": 0x8847
    }

    ip_protocol_map = {
        "IP_TCP": 6,
        "IP_ICMP": 1,
        "IP_UDP": 17,
        "IP_IGMP": 2,
        "IP_PIM": 103,
        "IP_RSVP": 46,
        "IP_GRE": 47,
        "IP_AUTH": 51,
        "IP_L2TP": 115
    }

    def __init__(self):
        self.yang_acl = None
        self.requested_session = None
        self.tables_db_info = {}
        self.rules_db_info = {}
        self.rules_info = {}
        self.sessions_db_info = {}
        self.configdb = ConfigDBConnector()
        self.configdb.connect()

        self.read_tables_info()
        self.read_rules_info()
        self.read_sessions_info()

    def read_tables_info(self):
        """
        Read ACL tables information from Config DB
        :return:
        """
        self.tables_db_info = self.configdb.get_table(self.ACL_TABLE)

    def get_tables_db_info(self):
        return self.tables_db_info

    def read_rules_info(self):
        """
        Read rules information from Config DB
        :return:
        """
        self.rules_db_info = self.configdb.get_table(self.ACL_RULE)

    def get_rules_db_info(self):
        return self.rules_db_info

    def read_sessions_info(self):
        """
        Read ACL tables information from Config DB
        :return:
        """
        self.sessions_db_info = self.configdb.get_table(self.MIRROR_SESSION)

    def get_sessions_db_info(self):
        """
        Read mirror session information from Config DB
        :return:
        """
        return self.sessions_db_info

    def get_session_name(self):
        """
        Read mirror session name from Config DB
        :return: Mirror session name
        """
        if self.requested_session:
            return self.requested_session

        for key in self.get_sessions_db_info():
            if key.startswith(self.SESSION_PREFIX):
                return key

        return None

    def set_session_name(self, session_name):
        """
        Set session name to se used in ACL rule action.
        :param session_name: Mirror session name
        """
        if session_name not in self.get_sessions_db_info():
            raise AclLoaderException("Session %s does not exist" % session_name)

        self.requested_session = session_name

    def set_max_priority(self, priority):
        """
        Set rules max priority
        :param priority: Rules max priority
        :return:
        """
        self.max_priority = int(priority)

    def is_table_valid(self, tname):
        return self.tables_db_info.get(tname)

    def is_table_mirror(self, tname):
        """
        Check if ACL table type is MIRROR
        :param tname: ACL table name
        :return: True if table type is MIRROR else False
        """
        return self.tables_db_info[tname]['type'].upper() == "MIRROR"

    def load_rules_from_file(self, filename):
        """
        Load file with ACL rules configuration in openconfig ACL format. Convert rules
        to Config DB schema.
        :param filename: File in openconfig ACL format
        :return:
        """
        self.yang_acl = pybindJSON.load(filename, openconfig_acl, "openconfig_acl")
        self.convert_rules()

    def convert_action(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.actions.config.forwarding_action == "ACCEPT":
            if self.is_table_mirror(table_name):
                session_name = self.get_session_name()
                if not session_name:
                    raise AclLoaderException("Mirroring session does not exist")

                rule_props["MIRROR_ACTION"] = session_name
            else:
                rule_props["PACKET_ACTION"] = "FORWARD"
        elif rule.actions.config.forwarding_action == "DROP":
            rule_props["PACKET_ACTION"] = "DROP"
        elif rule.actions.config.forwarding_action == "REJECT":
            rule_props["PACKET_ACTION"] = "DROP"
        else:
            raise AclLoaderException("Unknown rule action %s in table %s, rule %d" % (
                rule.actions.config.forwarding_action, table_name, rule_idx))

        return rule_props

    def convert_l2(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.l2.config.ethertype:
            if rule.l2.config.ethertype in self.ethertype_map:
                rule_props["ETHER_TYPE"] = self.ethertype_map[rule.l2.config.ethertype]
            else:
                try:
                    rule_props["ETHER_TYPE"] = int(rule.l2.config.ethertype)
                except:
                    raise AclLoaderException("Failed to convert ethertype %s table %s rule %s" % (
                        rule.l2.config.ethertype, table_name, rule_idx))

        return rule_props

    def convert_ipv4(self, table_name, rule_idx, rule):
        rule_props = {}

        if rule.ip.config.protocol:
            if self.ip_protocol_map.has_key(rule.ip.config.protocol):
                rule_props["IP_PROTOCOL"] = self.ip_protocol_map[rule.ip.config.protocol]
            else:
                try:
                    int(rule.ip.config.protocol)
                except:
                    raise AclLoaderException("Unknown rule protocol %s in table %s, rule %d!" % (
                        rule.ip.config.protocol, table_name, rule_idx))

                rule_props["IP_PROTOCOL"] = rule.ip.config.protocol

        if rule.ip.config.source_ip_address:
            rule_props["SRC_IP"] = rule.ip.config.source_ip_address

        if rule.ip.config.destination_ip_address:
            rule_props["DST_IP"] = rule.ip.config.destination_ip_address

        # NOTE: DSCP is available only for MIRROR table
        if self.is_table_mirror(table_name):
            if rule.ip.config.dscp:
                rule_props["DSCP"] = rule.ip.config.dscp

        return rule_props

    def convert_port(self, port):
        if ".." in port:
            return  port.replace("..", "-"), True
        else:
            return port, False

    def convert_transport(self,  table_name, rule_idx, rule):
        rule_props = {}

        if rule.transport.config.source_port:
            port, is_range = self.convert_port(str(rule.transport.config.source_port))
            rule_props["L4_SRC_PORT_RANGE" if is_range else "L4_SRC_PORT"] = port
        if rule.transport.config.destination_port:
            port, is_range = self.convert_port(str(rule.transport.config.destination_port))
            rule_props["L4_DST_PORT_RANGE" if is_range else "L4_DST_PORT"] = port

        tcp_flags = 0x00

        for flag in rule.transport.config.tcp_flags:
            if flag == "TCP_FIN":
                tcp_flags = tcp_flags | 0x01
            if flag == "TCP_SYN":
                tcp_flags = tcp_flags | 0x02
            if flag == "TCP_RST":
                tcp_flags = tcp_flags | 0x04
            if flag == "TCP_PSH":
                tcp_flags = tcp_flags | 0x08
            if flag == "TCP_ACK":
                tcp_flags = tcp_flags | 0x10
            if flag == "TCP_URG":
                tcp_flags = tcp_flags | 0x20
            if flag == "TCP_ECE":
                tcp_flags = tcp_flags | 0x40
            if flag == "TCP_CWR":
                tcp_flags = tcp_flags | 0x80

        if tcp_flags:
            rule_props["TCP_FLAGS"] = '0x{:02x}/0x{:02x}'.format(tcp_flags, tcp_flags)

        return rule_props

    def convert_rule_to_db_schema(self, table_name, rule):
        """
        Convert rules format from openconfig ACL to Config DB schema
        :param table_name: ACL table name to which rule belong
        :param rule: ACL rule in openconfig format
        :return: dict with Config DB schema
        """
        rule_idx = int(rule.config.sequence_id)
        rule_props = {}
        rule_data = {(table_name, "RULE_" + str(rule_idx)): rule_props}

        rule_props["PRIORITY"] = self.max_priority - rule_idx

        deep_update(rule_props, self.convert_action(table_name, rule_idx, rule))
        deep_update(rule_props, self.convert_l2(table_name, rule_idx, rule))
        deep_update(rule_props, self.convert_ipv4(table_name, rule_idx, rule))
        deep_update(rule_props, self.convert_transport(table_name, rule_idx, rule))

        return rule_data

    def deny_rule(self, table_name):
        """
        Create default deny rule in Config DB format
        :param table_name: ACL table name to which rule belong
        :return: dict with Config DB schema
        """
        rule_props = {}
        rule_data = {(table_name, "DEFAULT_RULE"): rule_props}
        rule_props["PRIORITY"] = self.min_priority
        rule_props["ETHER_TYPE"] = "0x0800"
        rule_props["PACKET_ACTION"] = "DROP"
        return rule_data

    def convert_rules(self):
        """
        Convert rules in openconfig ACL format to Config DB schema
        :return:
        """
        for acl_set_name in self.yang_acl.acl.acl_sets.acl_set:
            table_name = acl_set_name.replace(" ", "_").replace("-", "_").upper()
            acl_set = self.yang_acl.acl.acl_sets.acl_set[acl_set_name]

            if not self.is_table_valid(table_name):
                warning("%s table does not exist" % (table_name))
                continue

            for acl_entry_name in acl_set.acl_entries.acl_entry:
                acl_entry = acl_set.acl_entries.acl_entry[acl_entry_name]
                rule = self.convert_rule_to_db_schema(table_name, acl_entry)
                deep_update(self.rules_info, rule)

            if not self.is_table_mirror(table_name):
                deep_update(self.rules_info, self.deny_rule(table_name))

    def full_update(self):
        """
        Perform full update of ACL rules configuration. All existing rules
        will be removed. New rules loaded from file will be installed.
        :return:
        """
        for key in self.rules_db_info.keys():
            self.configdb.mod_entry(self.ACL_RULE, key, None)

        self.configdb.mod_config({self.ACL_RULE: self.rules_info})

    def incremental_update(self):
        """
        Perform incremental ACL rules configuration update. Get existing rules from
        Config DB. Compare with rules specified in file and perform corresponding
        modifications.
        :return:
        """
        new_rules = set(self.rules_info.iterkeys())
        current_rules = set(self.rules_db_info.iterkeys())

        added_rules = new_rules.difference(current_rules)
        removed_rules = current_rules.difference(new_rules)
        existing_rules = new_rules.intersection(current_rules)

        for key in removed_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, None)

        for key in added_rules:
            self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])

        for key in existing_rules:
            if cmp(self.rules_info[key], self.rules_db_info[key]):
                self.configdb.mod_entry(self.ACL_RULE, key, None)
                self.configdb.mod_entry(self.ACL_RULE, key, self.rules_info[key])

    def show_table(self, table_name):
        """
        Show ACL table configuration.
        :param table_name: Optional. ACL table name. Filter tables by specified name.
        :return:
        """
        header = ("Name", "Type", "Ports", "Description")

        data = []
        for key, val in self.get_tables_db_info().iteritems():
            if table_name and key != table_name:
                continue

            if not val["ports"]:
                data.append([key, val["type"], "", val["policy_desc"]])
            else:
                ports = natsorted(val["ports"])
                data.append([key, val["type"], ports[0], val["policy_desc"]])

                if len(ports) > 1:
                    for port in ports[1:]:
                        data.append(["", "", port, ""])

        print(tabulate.tabulate(data, headers=header, tablefmt="simple", missingval=""))

    def show_session(self, session_name):
        """
        Show mirror session configuration.
        :param session_name: Optional. Mirror session name. Filter sessions by specified name.
        :return:
        """
        header = ("Name", "SRC IP", "DST IP", "GRE", "DSCP", "TTL", "Queue")

        data = []
        for key, val in self.get_sessions_db_info().iteritems():
            if session_name and key != session_name:
                continue

            data.append([key, val["src_ip"], val["dst_ip"],
                         val.get("gre_type", ""), val.get("dscp", ""),
                         val.get("ttl", ""), val.get("queue", "")])

        print(tabulate.tabulate(data, headers=header, tablefmt="simple", missingval=""))

    def show_rule(self, table_name, rule_id):
        """
        Show ACL rules configuration.
        :param table_name: Optional. ACL table name. Filter rules by specified table name.
        :param rule_id: Optional. ACL rule name. Filter rule by specified rule name.
        :return:
        """
        header = ("Rule ID", "Table Name", "Priority", "Action", "Match")

        ignore_list = ["PRIORITY", "PACKET_ACTION", "MIRROR_ACTION"]

        raw_data = []
        for (tname, rid), val in self.get_rules_db_info().iteritems():

            if table_name and table_name != tname:
                continue

            if rule_id and rule_id != rid:
                continue

            priority = val["PRIORITY"]

            action = ""
            if "PACKET_ACTION" in val:
                action = val["PACKET_ACTION"]
            elif "MIRROR_ACTION" in val:
                action = "MIRROR: %s" % val["MIRROR_ACTION"]
            else:
                continue

            matches = ["%s: %s" % (k, v) for k, v in val.iteritems() if k not in ignore_list]

            matches.sort()

            rule_data = [[tname, rid, priority, action, matches[0]]]
            if len(matches) > 1:
                for m in matches[1:]:
                    rule_data.append(["", "", "", "", m])

            raw_data.append([priority, rule_data])

        def cmp_rules(a, b):
            return cmp(a[0], b[0])

        raw_data.sort(cmp_rules)
        raw_data.reverse()

        data = []
        for _, d in raw_data:
            data += d

        print(tabulate.tabulate(data, headers=header, tablefmt="simple", missingval=""))