diff --git a/src/service/service/service_handlers/p4_fabric_tna_commons/p4_fabric_tna_commons.py b/src/service/service/service_handlers/p4_fabric_tna_commons/p4_fabric_tna_commons.py new file mode 100644 index 0000000000000000000000000000000000000000..eb8077ff37b5eda29308517d1fb3aa485b80027c --- /dev/null +++ b/src/service/service/service_handlers/p4_fabric_tna_commons/p4_fabric_tna_commons.py @@ -0,0 +1,908 @@ +# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +""" +Common objects and methods for the SD-Fabric (fabric TNA) dataplane. +This dataplane covers both software based and hardware-based Stratum-enabled P4 switches, +such as the BMv2 software switch and Intel's Tofino/Tofino-2 switches. + +SD-Fabric repo: https://github.com/stratum/fabric-tna +SD-Fabric docs: https://docs.sd-fabric.org/master/index.html +""" + +import time +import logging +import struct +from common.proto.context_pb2 import ConfigActionEnum, ConfigRule +from common.tools.object_factory.ConfigRule import json_config_rule +from common.type_checkers.Checkers import chk_address_mac, chk_vlan_id, \ + chk_address_ipv4, chk_transport_port +from random import randint +from typing import List, Tuple + +LOGGER = logging.getLogger(__name__) + +# Common service handler settings +TARGET_P4_ARCH = "target_p4_arch" +SWITCH_DATAPLANE_ID_MAP = "switch_dataplane_id_map" +VLAN_ID = "vlan_id" + +# P4 architectures +TARGET_ARCH_TNA = "tna" +TARGET_ARCH_V1MODEL = "v1model" +SUPPORTED_TARGET_ARCH_LIST = [TARGET_ARCH_TNA, TARGET_ARCH_V1MODEL] + +# Recirculation ports for various targets +RECIRCULATION_PORTS_TNA = [68, 196, 324, 452] # Tofino-2 (2-pipe switches use only the first 2 entries) +RECIRCULATION_PORTS_V1MODEL = [510] # Variable FAKE_V1MODEL_RECIRC_PORT in p4 source program + +# P4 tables +TABLE_INGRESS_VLAN = "FabricIngress.filtering.ingress_port_vlan" +TABLE_EGRESS_VLAN = "FabricEgress.egress_next.egress_vlan" +TABLE_FWD_CLASSIFIER = "FabricIngress.filtering.fwd_classifier" +TABLE_BRIDGING = "FabricIngress.forwarding.bridging" +TABLE_ROUTING_V4 = "FabricIngress.forwarding.routing_v4" +TABLE_NEXT_SIMPLE = "FabricIngress.next.simple" +TABLE_NEXT_HASHED = "FabricIngress.next.hashed" +TABLE_ACL = "FabricIngress.acl.acl" + +# Action profile members +ACTION_PROFILE_NEXT_HASHED = "FabricIngress.next.hashed_profile" + +# Clone sessions +CLONE_SESSION = "/clone_sessions/clone_session" + +# Forwarding types +FORWARDING_TYPE_BRIDGING = 0 +FORWARDING_TYPE_MPLS = 1 +FORWARDING_TYPE_UNICAST_IPV4 = 2 +FORWARDING_TYPE_IPV4_MULTICAST = 3 +FORWARDING_TYPE_IPV6_UNICAST = 4 +FORWARDING_TYPE_IPV6_MULTICAST = 5 +FORWARDING_TYPE_UNKNOWN = 7 + +FORWARDING_TYPES_VALID = [ + FORWARDING_TYPE_BRIDGING, + FORWARDING_TYPE_MPLS, + FORWARDING_TYPE_UNICAST_IPV4, + FORWARDING_TYPE_IPV4_MULTICAST, + FORWARDING_TYPE_IPV6_UNICAST, + FORWARDING_TYPE_IPV6_MULTICAST, + FORWARDING_TYPE_UNKNOWN +] + +# Port types +PORT_TYPE_INT = "int" +PORT_TYPE_HOST = "host" +PORT_TYPE_SWITCH = "switch" + +PORT_TYPE_ACTION_EDGE = 1 +PORT_TYPE_ACTION_INFRA = 2 +PORT_TYPE_ACTION_INTERNAL = 3 + +PORT_TYPE_MAP = { + PORT_TYPE_INT: PORT_TYPE_ACTION_INTERNAL, + PORT_TYPE_HOST: PORT_TYPE_ACTION_EDGE, + PORT_TYPE_SWITCH: PORT_TYPE_ACTION_INFRA +} + +PORT_TYPES_STR_VALID = [PORT_TYPE_INT, PORT_TYPE_HOST, PORT_TYPE_SWITCH] +PORT_TYPES_INT_VALID = [PORT_TYPE_ACTION_EDGE, PORT_TYPE_ACTION_INFRA, PORT_TYPE_ACTION_INTERNAL] + +# Bridged metadata type +BRIDGED_MD_TYPE_EGRESS_MIRROR = 2 +BRIDGED_MD_TYPE_INGRESS_MIRROR = 3 +BRIDGED_MD_TYPE_INT_INGRESS_DROP = 4 +BRIDGED_MD_TYPE_DEFLECTED = 5 + +# Mirror types +MIRROR_TYPE_INVALID = 0 +MIRROR_TYPE_INT_REPORT = 1 + +# VLAN +VLAN_DEF = 4094 + +# Supported Ethernet types +ETHER_TYPE_IPV4 = "0x0800" +ETHER_TYPE_IPV6 = "0x86DD" + +# Member ID +NEXT_MEMBER_ID = 1 + +# Time interval in seconds for consecutive rule management (insert/delete) operations +RULE_CONF_INTERVAL_SEC = 0.1 + +################################################################################################################ +### Miscellaneous methods +################################################################################################################ + +def arch_tna(arch : str) -> bool: + return arch == TARGET_ARCH_TNA + +def arch_v1model(arch : str) -> bool: + return not arch_tna(arch) + +def generate_random_mac() -> str: + mac = [randint(0x00, 0xff)] * 6 + mac_str = ':'.join(map(lambda x: "%02x" % x, mac)) + chk_address_mac(mac_str), "Invalid MAC address generated" + + return mac_str + +def prefix_to_hex_mask(prefix_len): + # Calculate the binary mask + binary_mask = (1 << 32) - (1 << (32 - prefix_len)) + + # Convert the binary mask to the 4 octets (32 bits) + mask = struct.pack('!I', binary_mask) + + # Convert to a string of hex values + hex_mask = ''.join(f'{byte:02x}' for byte in mask) + + return "0x"+hex_mask.upper() + +def sleep_for(time_sec : int) -> None: + assert time_sec > 0, "Invalid sleep period in seconds" + time.sleep(time_sec) + +################################################################################################################ +### Rule generation methods +################################################################################################################ + +################################### +### A. Port setup +################################### + +def rules_set_up_port_ingress( + ingress_port : int, + port_type : str, + vlan_id: int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert ingress_port >= 0, "Invalid ingress port to configure ingress port" + assert port_type.lower() in PORT_TYPES_STR_VALID, "Invalid port type to configure ingress port" + assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure ingress port" + + rule_no = cache_rule(TABLE_INGRESS_VLAN, action) + + port_type_int = PORT_TYPE_MAP[port_type.lower()] + assert port_type_int in PORT_TYPES_INT_VALID, "Invalid port type to configure ingress filtering" + + rules_filtering_vlan_ingress = [] + rules_filtering_vlan_ingress.append( + json_config_rule( + action, + '/tables/table/'+TABLE_INGRESS_VLAN+'['+str(rule_no)+']', + { + 'table-name': TABLE_INGRESS_VLAN, + 'match-fields': [ + { + 'match-field': 'ig_port', + 'match-value': str(ingress_port) + }, + { + 'match-field': 'vlan_is_valid', + 'match-value': '0' + } + ], + 'action-name': 'FabricIngress.filtering.permit_with_internal_vlan', + 'action-params': [ + { + 'action-param': 'port_type', + 'action-value': str(port_type_int) + }, + { + 'action-param': 'vlan_id', + 'action-value': str(vlan_id) + } + ], + 'priority': 10 + } + ) + ) + + return rules_filtering_vlan_ingress + +def rules_set_up_port_egress( + egress_port : int, + vlan_id: int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid egress port to configure egress vlan" + assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure egress vlan" + + rule_no = cache_rule(TABLE_EGRESS_VLAN, action) + + rules_vlan_egress = [] + rules_vlan_egress.append( + json_config_rule( + action, + '/tables/table/'+TABLE_EGRESS_VLAN+'['+str(rule_no)+']', + { + 'table-name': TABLE_EGRESS_VLAN, + 'match-fields': [ + { + 'match-field': 'eg_port', + 'match-value': str(egress_port) + }, + { + 'match-field': 'vlan_id', + 'match-value': str(vlan_id) + } + ], + 'action-name': 'FabricEgress.egress_next.pop_vlan', + 'action-params': [] + } + ) + ) + + return rules_vlan_egress + +def rules_set_up_fwd_classifier( + ingress_port : int, + fwd_type : int, + eth_type: str, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert ingress_port >= 0, "Invalid ingress port to configure forwarding classifier" + assert fwd_type in FORWARDING_TYPES_VALID, "Invalid forwarding type to configure forwarding classifier" + + rule_no = cache_rule(TABLE_FWD_CLASSIFIER, action) + + rules_filtering_fwd_classifier = [] + rules_filtering_fwd_classifier.append( + json_config_rule( + action, + '/tables/table/'+TABLE_FWD_CLASSIFIER+'['+str(rule_no)+']', + { + 'table-name': TABLE_FWD_CLASSIFIER, + 'match-fields': [ + { + 'match-field': 'ig_port', + 'match-value': str(ingress_port) + }, + { + 'match-field': 'eth_type', + 'match-value': eth_type + } + ], + 'action-name': 'FabricIngress.filtering.set_forwarding_type', + 'action-params': [ + { + 'action-param': 'fwd_type', + 'action-value': str(FORWARDING_TYPE_UNICAST_IPV4) + }, + ], + 'priority': 10 + } + ) + ) + + return rules_filtering_fwd_classifier + +def rules_set_up_port( + port : int, + port_type : str, + fwd_type : int, + vlan_id : int, + action : ConfigActionEnum, # type: ignore + eth_type=ETHER_TYPE_IPV4): + rules_list = [] + + rules_list.extend( + rules_set_up_port_ingress( + ingress_port=port, + port_type=port_type, + vlan_id=vlan_id, + action=action + ) + ) + rules_list.extend( + rules_set_up_fwd_classifier( + ingress_port=port, + fwd_type=fwd_type, + eth_type=eth_type, + action=action + ) + ) + rules_list.extend( + rules_set_up_port_egress( + egress_port=port, + vlan_id=vlan_id, + action=action + ) + ) + LOGGER.debug("Port configured:{}".format(port)) + + return rules_list + +################################### +### A. End of port setup +################################### + + +################################### +### B. L2 setup +################################### + +def rules_set_up_fwd_bridging( + vlan_id: int, + eth_dst : str, + egress_port : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure bridging" + assert chk_address_mac(eth_dst), "Invalid destination Ethernet address to configure bridging" + assert egress_port >= 0, "Invalid outport to configure bridging" + + rule_no = cache_rule(TABLE_BRIDGING, action) + + rules_fwd_bridging = [] + rules_fwd_bridging.append( + json_config_rule( + action, + '/tables/table/'+TABLE_BRIDGING+'['+str(rule_no)+']', + { + 'table-name': TABLE_BRIDGING, + 'match-fields': [ + { + 'match-field': 'vlan_id', + 'match-value': str(vlan_id) + }, + { + 'match-field': 'eth_dst', + 'match-value': eth_dst + } + ], + 'action-name': 'FabricIngress.forwarding.set_next_id_bridging', + 'action-params': [ + { + 'action-param': 'next_id', + 'action-value': str(egress_port) + } + ], + 'priority': 1 + } + ) + ) + + return rules_fwd_bridging + +def rules_set_up_next_output_simple( + egress_port : int, + eth_src : str, + eth_dst : str, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport to configure next output simple" + assert chk_address_mac(eth_src), "Invalid source Ethernet address to configure next output simple" + assert chk_address_mac(eth_dst), "Invalid destination Ethernet address to configure next output simple" + + rule_no = cache_rule(TABLE_NEXT_SIMPLE, action) + + rules_next_output_simple = [] + rules_next_output_simple.append( + json_config_rule( + action, + '/tables/table/'+TABLE_NEXT_SIMPLE+'['+str(rule_no)+']', + { + 'table-name': TABLE_NEXT_SIMPLE, + 'match-fields': [ + { + 'match-field': 'next_id', + 'match-value': str(egress_port) + } + ], + 'action-name': 'FabricIngress.next.output_simple', + 'action-params': [ + { + 'action-param': 'port_num', + 'action-value': str(egress_port) + } + ] + } + ) + ) + + return rules_next_output_simple + +def rules_set_up_next_output_hashed( + egress_port : int, + action : ConfigActionEnum, # type: ignore + next_id = None) -> List [Tuple]: + assert egress_port >= 0, "Invalid outport to configure next output hashed" + + if next_id is None: + next_id = egress_port + + global NEXT_MEMBER_ID + + rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) + + rules_next_output_hashed = [] + rules_next_output_hashed.append( + json_config_rule( + action, + '/action_profiles/action_profile/'+ACTION_PROFILE_NEXT_HASHED+'['+str(rule_no)+']', + { + 'action-profile-name': ACTION_PROFILE_NEXT_HASHED, + 'member-id': NEXT_MEMBER_ID, + 'action-name': 'FabricIngress.next.output_hashed', + 'action-params': [ + { + 'action-param': 'port_num', + 'action-value': str(egress_port) + } + ] + } + ) + ) + + rule_no = cache_rule(TABLE_NEXT_HASHED, action) + + rules_next_output_hashed.append( + json_config_rule( + action, + '/tables/table/'+TABLE_NEXT_HASHED+'['+str(rule_no)+']', + { + 'table-name': TABLE_NEXT_HASHED, + 'member-id': NEXT_MEMBER_ID, + 'match-fields': [ + { + 'match-field': 'next_id', + 'match-value': str(next_id) + } + ] + } + ) + ) + + NEXT_MEMBER_ID += 1 + + return rules_next_output_hashed + +################################### +### B. End of L2 setup +################################### + + +################################### +### C. L3 setup +################################### + +def rules_set_up_routing( + ipv4_dst : str, + egress_port : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(ipv4_dst), "Invalid destination IPv4 address to configure routing" + assert egress_port >= 0, "Invalid outport to configure routing" + + rule_no = cache_rule(TABLE_ROUTING_V4, action) + + rules_routing = [] + rules_routing.append( + json_config_rule( + action, + '/tables/table/'+TABLE_ROUTING_V4+'['+str(rule_no)+']', + { + 'table-name': TABLE_ROUTING_V4, + 'match-fields': [ + { + 'match-field': 'ipv4_dst', + 'match-value': ipv4_dst + } + ], + 'action-name': 'FabricIngress.forwarding.set_next_id_routing_v4', + 'action-params': [ + { + 'action-param': 'next_id', + 'action-value': str(egress_port) + } + ] + } + ) + ) + + return rules_routing + +def rules_set_up_next_routing_simple( + egress_port : int, + eth_src : str, + eth_dst : str, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport to configure next routing simple" + assert chk_address_mac(eth_src), "Invalid source Ethernet address to configure next routing simple" + assert chk_address_mac(eth_dst), "Invalid destination Ethernet address to configure next routing simple" + + rule_no = cache_rule(TABLE_NEXT_SIMPLE, action) + + rules_next_routing_simple = [] + rules_next_routing_simple.append( + json_config_rule( + action, + '/tables/table/'+TABLE_NEXT_SIMPLE+'['+str(rule_no)+']', + { + 'table-name': TABLE_NEXT_SIMPLE, + 'match-fields': [ + { + 'match-field': 'next_id', + 'match-value': str(egress_port) + } + ], + 'action-name': 'FabricIngress.next.routing_simple', + 'action-params': [ + { + 'action-param': 'port_num', + 'action-value': str(egress_port) + }, + { + 'action-param': 'smac', + 'action-value': eth_src + }, + { + 'action-param': 'dmac', + 'action-value': eth_dst + } + ] + } + ) + ) + + return rules_next_routing_simple + +def rules_set_up_next_routing_hashed( + egress_port : int, + action : ConfigActionEnum, # type: ignore + next_id = None) -> List [Tuple]: + assert egress_port >= 0, "Invalid outport to configure next routing hashed" + random_mac_src = generate_random_mac() + random_mac_dst = generate_random_mac() + if next_id is None: + next_id = egress_port + + global NEXT_MEMBER_ID + + rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) + + rules_next_routing_hashed = [] + rules_next_routing_hashed.append( + json_config_rule( + action, + '/action_profiles/action_profile/'+ACTION_PROFILE_NEXT_HASHED+'['+str(rule_no)+']', + { + 'action-profile-name': ACTION_PROFILE_NEXT_HASHED, + 'member-id': NEXT_MEMBER_ID, + 'action-name': 'FabricIngress.next.routing_hashed', + 'action-params': [ + { + 'action-param': 'port_num', + 'action-value': str(egress_port) + }, + { + 'action-param': 'smac', + 'action-value': random_mac_src + }, + { + 'action-param': 'dmac', + 'action-value': random_mac_dst + } + ] + } + ) + ) + + rule_no = cache_rule(TABLE_NEXT_HASHED, action) + + rules_next_routing_hashed.append( + json_config_rule( + action, + '/tables/table/'+TABLE_NEXT_HASHED+'['+str(rule_no)+']', + { + 'table-name': TABLE_NEXT_HASHED, + 'member-id': NEXT_MEMBER_ID, + 'match-fields': [ + { + 'match-field': 'next_id', + 'match-value': str(next_id) + } + ] + } + ) + ) + + return rules_next_routing_hashed + +################################### +### C. End of L3 setup +################################### + + +################################### +### D. Flow mirroring +################################### + +def rules_set_up_report_mirror_flow( + recirculation_port_list : List, + report_mirror_id_list : List, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + rules_list = [] + + for i, mirror_id in enumerate(report_mirror_id_list): + LOGGER.debug("Mirror ID:{} - Recirculation port: {}".format( + mirror_id, recirculation_port_list[i])) + rules_list.extend( + rules_set_up_clone_session( + session_id=mirror_id, + egress_port=recirculation_port_list[i], + instance=0, + action=action + ) + ) + + return rules_list + +def rules_set_up_clone_session( + session_id : int, + egress_port : int, + instance : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert session_id >= 0, "Invalid session identifier to configure clone session" + assert egress_port >= 0, "Invalid egress port number to configure clone session" + assert instance >= 0, "Invalid instance number to configure clone session" + + rule_no = cache_rule(CLONE_SESSION, action) + + #TODO: For TNA pass also: packet_length_bytes = 128 + packet_length_bytes = 128 + + rules_clone_session = [] + + rules_clone_session.append( + json_config_rule( + action, + CLONE_SESSION+'['+str(rule_no)+']', + { + 'session-id': session_id, + 'replicas': [ + { + 'egress-port': egress_port, + 'instance': instance + } + ] + } + ) + ) + + return rules_clone_session + +################################### +### D. End of flow mirroring +################################### + + +################################### +### E. Access Control Lists +################################### + +def rules_set_up_acl_filter_host( + ingress_port : int, + ip_address : str, + prefix_len : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert ingress_port >= 0, "Invalid ingress port to configure ACL" + assert chk_address_ipv4(ip_address), "Invalid IP address to configure ACL" + assert 0 < prefix_len <= 32, "Invalid IP address prefix length to configure ACL" + + prefix_len_hex = prefix_to_hex_mask(prefix_len) + + rule_no = cache_rule(TABLE_ACL, action) + + rules_acl = [] + rules_acl.append( + json_config_rule( + action, + '/tables/table/'+TABLE_ACL+'['+str(rule_no)+']', + { + 'table-name': TABLE_ACL, + 'match-fields': [ + { + 'match-field': 'ig_port', + 'match-value': str(ingress_port) + }, + { + 'match-field': 'ipv4_src', + 'match-value': '%s&&&%s' % (ip_address, prefix_len_hex) + } + ], + 'action-name': 'FabricIngress.acl.drop', + 'action-params': [], + 'priority': 1 + } + ) + ) + + return rules_acl + +def rules_set_up_acl_filter_port( + ingress_port : int, + transport_port : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert ingress_port >= 0, "Invalid ingress port to configure ACL" + assert chk_transport_port(transport_port), "Invalid transport port to configure ACL" + + rule_no = cache_rule(TABLE_ACL, action) + + rules_acl = [] + rules_acl.append( + json_config_rule( + action, + '/tables/table/'+TABLE_ACL+'['+str(rule_no)+']', + { + 'table-name': TABLE_ACL, + 'match-fields': [ + { + 'match-field': 'ig_port', + 'match-value': str(ingress_port) + }, + { + 'match-field': 'l4_dport', + 'match-value': str(transport_port) + } + ], + 'action-name': 'FabricIngress.acl.drop', + 'action-params': [], + 'priority': 1 + } + ) + ) + + return rules_acl + +########################################### +### E. End of Access Control Lists +########################################### + +################################################################################################################ +### Rule management methods +################################################################################################################ + +def apply_rules(task_executor, device_obj, json_config_rules): + applied_rules = 0 + failed_rules = 0 + total_rules = len(json_config_rules) + assert device_obj, "Cannot apply rules to invalid device object" + + if total_rules == 0: + return applied_rules, failed_rules + + # Provision rules one-by-one + for i, json_config_rule in enumerate(json_config_rules): + LOGGER.debug("Applying rule #{}: {}".format(i, json_config_rule)) + try: + # Cleanup the rules of this particular object + del device_obj.device_config.config_rules[:] + + # Add the new rule to apply + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + + # Configure the device via the SBI + # TODO: Acquire status of this RPC to ensure that the rule is actually applied + task_executor.configure_device(device_obj) + + # Sleep for some time till the next operation + sleep_for(RULE_CONF_INTERVAL_SEC) + + applied_rules += 1 + except Exception as ex: + LOGGER.error("Error while applying rule #{}: {}".format(i, ex)) + failed_rules += 1 + + LOGGER.info("Batch rules: {}/{} applied".format(applied_rules, total_rules)) + LOGGER.info("Batch rules: {}/{} failed".format(failed_rules, total_rules)) + + return applied_rules, failed_rules + +# Map for keeping rule counts per table +RULE_ENTRY_MAP = {} + +def cache_rule( + table_name : str, + action : ConfigActionEnum) -> int: # type: ignore + rule_no = -1 + + if action == ConfigActionEnum.CONFIGACTION_SET: + rule_no = add_rule_to_map(table_name) + elif action == ConfigActionEnum.CONFIGACTION_DELETE: + rule_no = delete_rule_from_map(table_name) + else: + assert True, "Invalid rule configuration action" + + assert rule_no > 0, "Invalid rule identifier to configure table {}".format(table_name) + + return rule_no + +def add_rule_to_map(table_name : str) -> int: + if table_name not in RULE_ENTRY_MAP: + RULE_ENTRY_MAP[table_name] = [] + + # Current number of rules + rules_no = len(RULE_ENTRY_MAP[table_name]) + + # Get a new valid rule index + new_index = find_minimum_available_rule_index(RULE_ENTRY_MAP[table_name]) + LOGGER.debug("Minimum available rule index for table {} is: {}".format(table_name, new_index)) + assert new_index > 0, "Invalid rule index for table {}".format(table_name) + + # New entry + new_rule_entry = table_name+"["+str(new_index)+"]" + + # Add entry to the list + RULE_ENTRY_MAP[table_name].append(new_rule_entry) + assert len(RULE_ENTRY_MAP[table_name]) == rules_no + 1 + + return new_index + +def delete_rule_from_map(table_name : str) -> int: + if table_name not in RULE_ENTRY_MAP: + LOGGER.error("Table {} has no entries".format(table_name)) + return -1 + + # Current number of rules + rules_no = len(RULE_ENTRY_MAP[table_name]) + + # Remove last rule + rule_entry = RULE_ENTRY_MAP[table_name].pop() + # Get its index + rule_no = int(rule_entry.split('[')[1].split(']')[0]) + + assert len(RULE_ENTRY_MAP[table_name]) == rules_no - 1 + + # Return the index of the removed rule + return rule_no + +def string_contains_number(input_string : str, target_number : int) -> bool: + return str(target_number) in input_string + +def rule_index_exists(rule_entry_list : List, target_rule_index : int) -> bool: + # Rule indices start from 1 + if target_rule_index <= 0: + return False + + rules_no = len(rule_entry_list) + if rules_no == 0: + return False + + for rule in rule_entry_list: + if string_contains_number(rule, target_rule_index): + return True + + return False + +def find_minimum_available_rule_index(rule_entry_list : List) -> int: + rules_no = len(rule_entry_list) + if rules_no == 0: + return 1 + + min_index = -1 + for i, _ in enumerate(rule_entry_list): + index = i+1 + idx_exists = rule_index_exists(rule_entry_list, index) + # This index is not present in the rule list, so it is available + if not idx_exists and min_index < index: + min_index = index + + # All of the existing rule indices are taken, proceed to the next one + if min_index == -1: + min_index = rules_no + 1 + + return min_index + +def print_rule_map() -> None: + for k in RULE_ENTRY_MAP.keys(): + LOGGER.info("Table {} entries: {}".format(k, RULE_ENTRY_MAP[k]))