From 5a393a5a82d1cd3038c87b59763bfbc426ae0ab6 Mon Sep 17 00:00:00 2001 From: "Georgios P. Katsikas" Date: Tue, 20 Jan 2026 00:53:47 +0200 Subject: [PATCH 1/3] feat: P4 UPF service handler based on SD-Fabric --- proto/context.proto | 1 + src/common/DeviceTypes.py | 3 + .../database/models/enums/ServiceType.py | 1 + .../service_handler_api/FilterFields.py | 46 +- .../service/service_handlers/__init__.py | 7 + .../p4_fabric_tna_commons.py | 218 ++-- .../p4_fabric_tna_l2_simple_config.py | 4 - .../p4_fabric_tna_upf/__init__.py | 13 + .../p4_fabric_tna_upf_config.py | 476 +++++++++ .../p4_fabric_tna_upf_service_handler.py | 928 ++++++++++++++++++ .../service/static/topology_icons/oran-cn.png | Bin 0 -> 23796 bytes .../service/static/topology_icons/oran-cu.png | Bin 0 -> 26326 bytes .../service/static/topology_icons/oran-du.png | Bin 0 -> 26185 bytes 13 files changed, 1593 insertions(+), 104 deletions(-) create mode 100644 src/service/service/service_handlers/p4_fabric_tna_upf/__init__.py create mode 100644 src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py create mode 100644 src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py create mode 100644 src/webui/service/static/topology_icons/oran-cn.png create mode 100644 src/webui/service/static/topology_icons/oran-cu.png create mode 100644 src/webui/service/static/topology_icons/oran-du.png diff --git a/proto/context.proto b/proto/context.proto index 62b21b449..3eebb54d4 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -359,6 +359,7 @@ enum ServiceTypeEnum { SERVICETYPE_IP_LINK = 11; SERVICETYPE_TAPI_LSP = 12; SERVICETYPE_IPOWDM = 13; + SERVICETYPE_UPF = 14; } enum ServiceStatusEnum { diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index 948de0e98..8e1cf6758 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -38,6 +38,9 @@ class DeviceTypeEnum(Enum): EMULATED_PACKET_ROUTER = 'emu-packet-router' EMULATED_PACKET_SWITCH = 'emu-packet-switch' EMULATED_XR_CONSTELLATION = 'emu-xr-constellation' + EMULATED_ORAN_DU = 'oran-du' + EMULATED_ORAN_CU = 'oran-cu' + EMULATED_ORAN_CN = 'oran-cn' # Real device types CLIENT = 'client' diff --git a/src/context/service/database/models/enums/ServiceType.py b/src/context/service/database/models/enums/ServiceType.py index c672d160f..cf24a000f 100644 --- a/src/context/service/database/models/enums/ServiceType.py +++ b/src/context/service/database/models/enums/ServiceType.py @@ -36,6 +36,7 @@ class ORM_ServiceTypeEnum(enum.Enum): IP_LINK = ServiceTypeEnum.SERVICETYPE_IP_LINK IPOWDM = ServiceTypeEnum.SERVICETYPE_IPOWDM TAPI_LSP = ServiceTypeEnum.SERVICETYPE_TAPI_LSP + UPF = ServiceTypeEnum.SERVICETYPE_UPF grpc_to_enum__service_type = functools.partial( grpc_to_enum, ServiceTypeEnum, ORM_ServiceTypeEnum) diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index b0a5666a6..2ce77608a 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -20,8 +20,50 @@ class FilterFieldEnum(Enum): SERVICE_TYPE = 'service_type' DEVICE_DRIVER = 'device_driver' -# Map allowed filter fields to allowed values per Filter field. -# If no restriction (free text) None is specified +SERVICE_TYPE_VALUES = { + ServiceTypeEnum.SERVICETYPE_UNKNOWN, + ServiceTypeEnum.SERVICETYPE_L3NM, + ServiceTypeEnum.SERVICETYPE_L2NM, + ServiceTypeEnum.SERVICETYPE_L1NM, + ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, + ServiceTypeEnum.SERVICETYPE_TE, + ServiceTypeEnum.SERVICETYPE_E2E, + ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY, + ServiceTypeEnum.SERVICETYPE_QKD, + ServiceTypeEnum.SERVICETYPE_INT, + ServiceTypeEnum.SERVICETYPE_ACL, + ServiceTypeEnum.SERVICETYPE_IP_LINK, + ServiceTypeEnum.SERVICETYPE_IPOWDM, + ServiceTypeEnum.SERVICETYPE_TAPI_LSP, + ServiceTypeEnum.SERVICETYPE_UPF, +} + +DEVICE_DRIVER_VALUES = { + DeviceDriverEnum.DEVICEDRIVER_UNDEFINED, + DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG, + DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API, + DeviceDriverEnum.DEVICEDRIVER_P4, + DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY, + DeviceDriverEnum.DEVICEDRIVER_ONF_TR_532, + DeviceDriverEnum.DEVICEDRIVER_XR, + DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN, + DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, + DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS, + DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, + DeviceDriverEnum.DEVICEDRIVER_OC, + DeviceDriverEnum.DEVICEDRIVER_QKD, + DeviceDriverEnum.DEVICEDRIVER_IETF_L3VPN, + DeviceDriverEnum.DEVICEDRIVER_IETF_SLICE, + DeviceDriverEnum.DEVICEDRIVER_NCE, + DeviceDriverEnum.DEVICEDRIVER_SMARTNIC, + DeviceDriverEnum.DEVICEDRIVER_MORPHEUS, + DeviceDriverEnum.DEVICEDRIVER_RYU, + DeviceDriverEnum.DEVICEDRIVER_GNMI_NOKIA_SRLINUX, + DeviceDriverEnum.DEVICEDRIVER_OPENROADM, + DeviceDriverEnum.DEVICEDRIVER_RESTCONF_OPENCONFIG, +} + +# Map allowed filter fields to allowed values per Filter field. If no restriction (free text) None is specified FILTER_FIELD_ALLOWED_VALUES = { FilterFieldEnum.SERVICE_TYPE.value : set(ServiceTypeEnum.values()), FilterFieldEnum.DEVICE_DRIVER.value : set(DeviceDriverEnum.values()), diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index c30d5c308..7a607acdd 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -32,6 +32,7 @@ from .p4_fabric_tna_int.p4_fabric_tna_int_service_handler import P4FabricINTServ from .p4_fabric_tna_l2_simple.p4_fabric_tna_l2_simple_service_handler import P4FabricL2SimpleServiceHandler from .p4_fabric_tna_l3.p4_fabric_tna_l3_service_handler import P4FabricL3ServiceHandler from .p4_fabric_tna_acl.p4_fabric_tna_acl_service_handler import P4FabricACLServiceHandler +from .p4_fabric_tna_upf.p4_fabric_tna_upf_service_handler import P4FabricUPFServiceHandler from .tapi_lsp.Tapi_LSPServiceHandler import Tapi_LSPServiceHandler from .tapi_tapi.TapiServiceHandler import TapiServiceHandler from .tapi_xr.TapiXrServiceHandler import TapiXrServiceHandler @@ -153,6 +154,12 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER: DeviceDriverEnum.DEVICEDRIVER_P4, } ]), + (P4FabricUPFServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE: ServiceTypeEnum.SERVICETYPE_UPF, + FilterFieldEnum.DEVICE_DRIVER: DeviceDriverEnum.DEVICEDRIVER_P4, + } + ]), (L2NM_IETFL2VPN_ServiceHandler, [ { FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_L2NM, 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 index b003ceef8..08f0536c2 100644 --- 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 @@ -48,7 +48,11 @@ VLAN_ID = "vlan_id" RECIRCULATION_PORT_LIST = "recirculation_port_list" PORT_LIST = "port_list" PORT_PREFIX = "port-" +FORWARDING_LIST = "fwd_list" ROUTING_LIST = "routing_list" +HOST_LIST = "host_list" +HOST_MAC = "host_mac" +HOST_LABEL = "host_label" MAC_SRC = "mac_src" MAC_DST = "mac_dst" IPV4_SRC = "ipv4_src" @@ -72,6 +76,7 @@ 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_PRE_NEXT_VLAN = "FabricIngress.pre_next.next_vlan" TABLE_NEXT_SIMPLE = "FabricIngress.next.simple" TABLE_NEXT_HASHED = "FabricIngress.next.hashed" TABLE_ACL = "FabricIngress.acl.acl" @@ -136,9 +141,6 @@ DEF_VLAN = 4094 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 @@ -421,60 +423,59 @@ def rules_set_up_fwd_bridging( return rules_fwd_bridging -def rules_set_up_next_output_simple( - egress_port : int, - action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert egress_port >= 0, "Invalid outport to configure next output simple" +def rules_set_up_pre_next_vlan( + next_id : int, + vlan_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert next_id >= 0, "Invalid next ID to configure pre-next VLAN" + assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure pre-next VLAN" - rule_no = cache_rule(TABLE_NEXT_SIMPLE, action) + rule_no = cache_rule(TABLE_PRE_NEXT_VLAN, action) - rules_next_output_simple = [] - rules_next_output_simple.append( + rules_pre_next_vlan = [] + rules_pre_next_vlan.append( json_config_rule( action, - '/tables/table/'+TABLE_NEXT_SIMPLE+'['+str(rule_no)+']', + '/tables/table/'+TABLE_PRE_NEXT_VLAN+'['+str(rule_no)+']', { - 'table-name': TABLE_NEXT_SIMPLE, + 'table-name': TABLE_PRE_NEXT_VLAN, 'match-fields': [ { 'match-field': 'next_id', - 'match-value': str(egress_port) + 'match-value': str(next_id) } ], - 'action-name': 'FabricIngress.next.output_simple', + 'action-name': 'FabricIngress.pre_next.set_vlan', 'action-params': [ { - 'action-param': 'port_num', - 'action-value': str(egress_port) + 'action-param': 'vlan_id', + 'action-value': str(vlan_id) } - ] + ], + 'priority': 0 } ) ) - return rules_next_output_simple + return rules_pre_next_vlan -def rules_set_up_next_output_hashed( +def rules_set_up_next_profile_hashed_output( 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 + next_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport to configure next profile for hashed output" + assert next_id >=0, "Invalid next ID to configure next profile for hashed output" rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) - rules_next_output_hashed = [] - rules_next_output_hashed.append( + rules_next_profile_hashed_out = [] + rules_next_profile_hashed_out.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, + 'member-id': next_id, 'action-name': 'FabricIngress.next.output_hashed', 'action-params': [ { @@ -486,15 +487,58 @@ def rules_set_up_next_output_hashed( ) ) + return rules_next_profile_hashed_out + +def rules_set_up_next_output_simple( + egress_port : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport 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_hashed( + egress_port : int, + next_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport to configure next routing hashed" + assert next_id >=0, "Invalid next ID to configure next routing hashed" + rule_no = cache_rule(TABLE_NEXT_HASHED, action) - rules_next_output_hashed.append( + rules_next_hashed = [] + rules_next_hashed.append( json_config_rule( action, '/tables/table/'+TABLE_NEXT_HASHED+'['+str(rule_no)+']', { 'table-name': TABLE_NEXT_HASHED, - 'member-id': NEXT_MEMBER_ID, + 'member-id': next_id, 'match-fields': [ { 'match-field': 'next_id', @@ -505,9 +549,7 @@ def rules_set_up_next_output_hashed( ) ) - NEXT_MEMBER_ID += 1 - - return rules_next_output_hashed + return rules_next_hashed ################################### ### B. End of L2 setup @@ -518,13 +560,55 @@ def rules_set_up_next_output_hashed( ### C. L3 setup ################################### +def rules_set_up_next_profile_hashed_routing( + egress_port : int, + next_id : int, + eth_src : str, + eth_dst : str, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert egress_port >= 0, "Invalid outport to configure next profile for hashed routing" + assert next_id >=0, "Invalid next ID to configure next profile for hashed routing" + assert chk_address_mac(eth_src), "Invalid source Ethernet address to configure next profile for hashed routing" + assert chk_address_mac(eth_dst), "Invalid destination Ethernet address to configure next profile for hashed routing" + + rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) + + rules_next_profile_hashed_rt = [] + rules_next_profile_hashed_rt.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_id, + 'action-name': 'FabricIngress.next.routing_hashed', + '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_profile_hashed_rt + def rules_set_up_routing( ipv4_dst : str, ipv4_prefix_len : int, egress_port : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore assert chk_address_ipv4(ipv4_dst), "Invalid destination IPv4 address to configure routing" - assert chk_prefix_len_ipv4(ipv4_prefix_len), "Invalid IPv4 prefix length" + assert chk_prefix_len_ipv4(ipv4_prefix_len), "Invalid IPv4 prefix length to configure routing" assert egress_port >= 0, "Invalid outport to configure routing" rule_no = cache_rule(TABLE_ROUTING_V4, action) @@ -600,68 +684,6 @@ def rules_set_up_next_routing_simple( 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 ################################### diff --git a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py b/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py index 0fd1b7101..d2a13ef03 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py +++ b/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py @@ -25,10 +25,6 @@ from common.proto.context_pb2 import ConfigActionEnum from service.service.service_handlers.p4_fabric_tna_commons.p4_fabric_tna_commons import * -# L2 simple service handler settings -FORWARDING_LIST = "fwd_list" -HOST_MAC = "host_mac" - def rules_set_up_port_host( port : int, vlan_id : int, diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/__init__.py b/src/service/service/service_handlers/p4_fabric_tna_upf/__init__.py new file mode 100644 index 000000000..023830645 --- /dev/null +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py new file mode 100644 index 000000000..feda2e79c --- /dev/null +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py @@ -0,0 +1,476 @@ +# Copyright 2022-2025 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 5G User Plane Function (UPF) offloading in P4 +based on the SD-Fabric dataplane model. +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 +""" + +from typing import List, Tuple +from common.proto.context_pb2 import ConfigActionEnum +from common.tools.object_factory.ConfigRule import json_config_rule +from common.type_checkers.Checkers import chk_address_ipv4, chk_prefix_len_ipv4, chk_transport_port + +from service.service.service_handlers.p4_fabric_tna_commons.p4_fabric_tna_commons import * + +# UPF service handler settings +UPF = "upf" +UPLINK_PORT = "uplink_port" +DOWNLINK_PORT = "downlink_port" +UPLINK_IP = "uplink_ip" +DOWNLINK_IP = "downlink_ip" +UPLINK_MAC = "uplink_mac" +DOWNLINK_MAC = "downlink_mac" +TEID = "teid" +SLICE_ID = "slice_id" +APP_ID = "app_id" +APP_METER_ID = "app_meter_id" +CTR_ID = "ctr_id" +TC_ID = "tc_id" +TUNNEL_PEER_ID = "tunnel_peer_id" +GNB = "gnb" +DATA_NETWORK = "data_network" +UE_LIST = "ue_list" +UE_ID = "ue_id" +UE_IP = "ue_ip" +PDU_LIST = "pdu_sessions" +QOS_FLOWS = "qos_flows" +PDU_SESSION_ID = "pdu_session_id" +DNN = "dnn" +PDU_SESSION_TYPE = "pdu_session_type" +GTPU_TUNNEL = "gtpu_tunnel" +UPLINK = "uplink" +DOWNLINK = "downlink" +SRC = "src" +DST = "dst" +QFI = "qfi" +FIVEQI = "5qi" +QOS_TYPE = "qos_type" +QOS_DESC = "qos_desc" + +# Tables +TABLE_UPF_INTERFACES = "FabricIngress.upf.interfaces" +TABLE_UPF_UL_SESSIONS = "FabricIngress.upf.uplink_sessions" +TABLE_UPF_UL_TERM = "FabricIngress.upf.uplink_terminations" +TABLE_UPF_UL_RECIRC_RULES = "FabricIngress.upf.uplink_recirc_rules" # No need for recirculation +TABLE_UPF_DL_SESSIONS = "FabricIngress.upf.downlink_sessions" +TABLE_UPF_DL_TERM = "FabricIngress.upf.downlink_terminations" +TABLE_UPF_DL_IG_TUN_PEERS = "FabricIngress.upf.ig_tunnel_peers" +TABLE_UPF_DL_EG_TUN_PEERS = "FabricEgress.upf.eg_tunnel_peers" +TABLE_UPF_DL_GTPU_ENCAP = "FabricEgress.upf.gtpu_encap" # This table has no key, thus auto-applies actions + +TABLE_QOS_SLICE_TC = "FabricIngress.qos.set_slice_tc" # This table is accessed automatically (no rule applied) +TABLE_QOS_DEF_TC = "FabricIngress.qos.default_tc" # Miss. No QoS applied so far +TABLE_QOS_QUEUES = "FabricIngress.qos.queues" # Miss. No QoS applied so far + +# UPF settings +GTP_PORT = 2152 + +GTPU_VALID = 1 +GTPU_INVALID = 0 + +## Default values +DEF_APP_ID = 0 +DEF_APP_METER_ID = 0 +DEF_CTR_ID = 401 +DEF_SLICE_ID = 0 +DEF_TC_ID = 3 +DEF_TEID = 1 +DEF_TUN_PEER_ID = 1 +DEF_SESSION_METER_ID = 0 +DEF_QFI = 0 + +# 5QI +FIVEQI_NON_GBR = 9 +FIVEQI_GBR = 1 +FIVEQI_DELAY_CRITICAL_GBR = 82 + +# QoS +QOS_TYPE_NON_GBR = "Non-GBR" +QOS_TYPE_GBR = "GBR" +QOS_TYPE_DELAY_CRITICAL_GBR = "Delay-Critical GBR" +QOS_TYPES_STR_VALID = [QOS_TYPE_NON_GBR, QOS_TYPE_GBR, QOS_TYPE_DELAY_CRITICAL_GBR] + +QOS_TYPE_TO_5QI_MAP = { + QOS_TYPE_NON_GBR: FIVEQI_NON_GBR, + QOS_TYPE_GBR: FIVEQI_GBR, + QOS_TYPE_DELAY_CRITICAL_GBR: FIVEQI_DELAY_CRITICAL_GBR +} + +QOS_TYPE_TO_DESC_MAP = { + QOS_TYPE_NON_GBR: "Best effort", + QOS_TYPE_GBR: "Low latency", + QOS_TYPE_DELAY_CRITICAL_GBR: "Ultra-low latency" +} + + +def rules_set_up_upf_interfaces( + ipv4_dst : str, + ipv4_prefix_len : int, + gtpu_value : int, + slice_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(ipv4_dst), "Invalid destination IPv4 address to configure UPF interface" + assert chk_prefix_len_ipv4(ipv4_prefix_len), "Invalid destination IPv4 address prefix length to configure UPF interface" + assert gtpu_value >= 0, "Invalid slice identifier to configure UPF interface" + assert slice_id >= 0, "Invalid slice identifier to configure UPF interface" + + action_name = None + + if gtpu_value == GTPU_VALID: # Packet carries a GTP header (UL packet) + action_name = "FabricIngress.upf.iface_access" + else: # Packet does not carry a GTP header (DL packet) + action_name = "FabricIngress.upf.iface_core" + + rule_no = cache_rule(TABLE_UPF_INTERFACES, action) + + rules_upf_interfaces = [] + rules_upf_interfaces.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_INTERFACES+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_INTERFACES, + 'match-fields': [ + { + 'match-field': 'ipv4_dst_addr', + 'match-value': ipv4_dst + "/" + str(ipv4_prefix_len) + }, + { + 'match-field': 'gtpu_is_valid', + 'match-value': str(gtpu_value) + } + ], + 'action-name': action_name, + 'action-params': [ + { + 'action-param': 'slice_id', + 'action-value': str(slice_id) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_interfaces + +################################### +### A. Uplink (UL) setup +################################### + +def rules_set_up_upf_uplink_sessions( + tun_ip_address : str, + teid : int, + session_meter_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(tun_ip_address), "Invalid tunnel IPv4 address to configure UPF uplink session" + assert teid >= 0, "Invalid tunnel endpoint identifier (TEID) to configure UPF uplink session" + assert session_meter_id >= 0, "Invalid session meter identifier to configure UPF uplink session" + + rule_no = cache_rule(TABLE_UPF_UL_SESSIONS, action) + + rules_upf_ul_session = [] + rules_upf_ul_session.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_UL_SESSIONS+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_UL_SESSIONS, + 'match-fields': [ + { + 'match-field': 'tunnel_ipv4_dst', + 'match-value': tun_ip_address + }, + { + 'match-field': 'teid', + 'match-value': str(teid) + } + ], + 'action-name': "FabricIngress.upf.set_uplink_session", + 'action-params': [ + { + 'action-param': 'session_meter_idx', + 'action-value': str(session_meter_id) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_ul_session + +def rules_set_up_upf_uplink_terminations( + ue_session_id : str, + app_id : int, + ctr_id : int, + app_meter_id : int, + tc_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(ue_session_id), "Invalid UE IPv4 address (UE session ID) to configure UPF uplink termination" + assert app_id >= 0, "Invalid application identifier to configure UPF uplink termination" + assert ctr_id >= 0, "Invalid ctr identifier to configure UPF uplink termination" + assert app_meter_id >= 0, "Invalid app meter identifier to configure UPF uplink termination" + assert tc_id >= 0, "Invalid tc identifier to configure UPF uplink termination" + + rule_no = cache_rule(TABLE_UPF_UL_TERM, action) + + rules_upf_ul_termination = [] + rules_upf_ul_termination.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_UL_TERM+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_UL_TERM, + 'match-fields': [ + { + 'match-field': 'ue_session_id', + 'match-value': ue_session_id + }, + { + 'match-field': 'app_id', + 'match-value': str(app_id) + } + ], + 'action-name': "FabricIngress.upf.app_fwd", + 'action-params': [ + { + 'action-param': 'ctr_id', + 'action-value': str(ctr_id) + }, + { + 'action-param': 'app_meter_idx', + 'action-value': str(app_meter_id) + }, + { + 'action-param': 'tc', + 'action-value': str(tc_id) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_ul_termination + +################################### +### A. End of Uplink (UL) setup +################################### + +################################### +### B. Downlink (DL) setup +################################### + +def rules_set_up_upf_downlink_sessions( + ipv4_dst : str, + session_meter_id : int, + tun_peer_id : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(ipv4_dst), "Invalid destination IPv4 address to configure UPF downlink session" + assert session_meter_id >= 0, "Invalid session meter identifier to configure UPF downlink session" + assert tun_peer_id >= 0, "Invalid tunnel peer identifier to configure UPF downlink session" + + rule_no = cache_rule(TABLE_UPF_DL_SESSIONS, action) + + rules_upf_dl_session = [] + rules_upf_dl_session.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_DL_SESSIONS+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_DL_SESSIONS, + 'match-fields': [ + { + 'match-field': 'ue_addr', + 'match-value': ipv4_dst + } + ], + 'action-name': "FabricIngress.upf.set_downlink_session", + 'action-params': [ + { + 'action-param': 'session_meter_idx', + 'action-value': str(session_meter_id) + }, + { + 'action-param': 'tun_peer_id', + 'action-value': str(tun_peer_id) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_dl_session + +def rules_set_up_upf_downlink_terminations( + ue_session_id : str, + app_id : int, + ctr_id : int, + app_meter_id : int, + tc_id : int, + teid : int, + qfi : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert chk_address_ipv4(ue_session_id), "Invalid UE IPv4 address (UE session ID) to configure UPF downlink termination" + assert app_id >= 0, "Invalid application identifier to configure downlink termination" + assert ctr_id >= 0, "Invalid ctr identifier to configure UPF downlink termination" + assert app_meter_id >= 0, "Invalid app meter identifier to configure UPF downlink termination" + assert tc_id >= 0, "Invalid tc identifier to configure UPF downlink termination" + assert teid >= 0, "Invalid tunnel endpoint identifier (TEID) to configure UPF downlink termination" + assert qfi >= 0, "Invalid QoS flow identifier (QFI) to configure UPF downlink termination" + + rule_no = cache_rule(TABLE_UPF_DL_TERM, action) + + rules_upf_dl_termination = [] + rules_upf_dl_termination.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_DL_TERM+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_DL_TERM, + 'match-fields': [ + { + 'match-field': 'ue_session_id', + 'match-value': ue_session_id + }, + { + 'match-field': 'app_id', + 'match-value': str(app_id) + } + ], + 'action-name': "FabricIngress.upf.downlink_fwd_encap", + 'action-params': [ + { + 'action-param': 'ctr_id', + 'action-value': str(ctr_id) + }, + { + 'action-param': 'app_meter_idx', + 'action-value': str(app_meter_id) + }, + { + 'action-param': 'tc', + 'action-value': str(tc_id) + }, + { + 'action-param': 'teid', + 'action-value': str(teid) + }, + { + 'action-param': 'qfi', + 'action-value': str(qfi) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_dl_termination + +def rules_set_up_upf_downlink_ig_tunnel_peers( + tun_peer_id : int, + tun_dst_addr : str, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert tun_peer_id >= 0, "Invalid tunnel peer identifier to configure UPF downlink ingress tunnel peers" + assert chk_address_ipv4(tun_dst_addr), "Invalid tunnel destination IPv4 address to configure UPF downlink ingress tunnel peers" + + rule_no = cache_rule(TABLE_UPF_DL_IG_TUN_PEERS, action) + + rules_upf_dl_ig_tun_peers = [] + rules_upf_dl_ig_tun_peers.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_DL_IG_TUN_PEERS+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_DL_IG_TUN_PEERS, + 'match-fields': [ + { + 'match-field': 'tun_peer_id', + 'match-value': str(tun_peer_id) + } + ], + 'action-name': "FabricIngress.upf.set_routing_ipv4_dst", + 'action-params': [ + { + 'action-param': 'tun_dst_addr', + 'action-value': tun_dst_addr + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_dl_ig_tun_peers + +def rules_set_up_upf_downlink_eg_tunnel_peers( + tun_peer_id : int, + tun_src_addr : str, + tun_dst_addr : str, + tun_src_port : int, + action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert tun_peer_id >= 0, "Invalid tunnel peer identifier to configure UPF downlink egress tunnel peers" + assert chk_address_ipv4(tun_src_addr), "Invalid tunnel source IPv4 address to configure UPF downlink egress tunnel peers" + assert chk_address_ipv4(tun_dst_addr), "Invalid tunnel destination IPv4 address to configure UPF downlink egress tunnel peers" + assert chk_transport_port(tun_src_port), "Invalid tunnel source transport port to configure UPF downlink egress tunnel peers" + + rule_no = cache_rule(TABLE_UPF_DL_EG_TUN_PEERS, action) + + rules_upf_dl_eg_tun_peers = [] + rules_upf_dl_eg_tun_peers.append( + json_config_rule( + action, + '/tables/table/'+TABLE_UPF_DL_EG_TUN_PEERS+'['+str(rule_no)+']', + { + 'table-name': TABLE_UPF_DL_EG_TUN_PEERS, + 'match-fields': [ + { + 'match-field': 'tun_peer_id', + 'match-value': str(tun_peer_id) + } + ], + 'action-name': "FabricEgress.upf.load_tunnel_params", + 'action-params': [ + { + 'action-param': 'tunnel_src_addr', + 'action-value': tun_src_addr + }, + { + 'action-param': 'tunnel_dst_addr', + 'action-value': tun_dst_addr + }, + { + 'action-param': 'tunnel_src_port', + 'action-value': str(tun_src_port) + } + ], + 'priority': 0 + } + ) + ) + + return rules_upf_dl_eg_tun_peers + +################################### +### B. End of Downlink (DL) setup +################################### diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py new file mode 100644 index 000000000..bb4afec97 --- /dev/null +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py @@ -0,0 +1,928 @@ +# 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. + +""" +Service handler for P4-based UPF offloading using the SD-Fabric P4 dataplane +for BMv2 and Intel Tofino switches. +""" + +import logging +from typing import Any, List, Dict, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigActionEnum, DeviceId, Service, Device +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type, chk_address_mac, chk_address_ipv4, chk_prefix_len_ipv4 +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.service_handlers.p4_fabric_tna_commons.p4_fabric_tna_commons import * +from service.service.task_scheduler.TaskExecutor import TaskExecutor + +from .p4_fabric_tna_upf_config import * + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'p4_fabric_tna_upf'}) + +class P4FabricUPFServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings # type: ignore + ) -> None: + """ Initialize Driver. + Parameters: + service + The service instance (gRPC message) to be managed. + task_executor + An instance of Task Executor providing access to the + service handlers factory, the context and device clients, + and an internal cache of already-loaded gRPC entities. + **settings + Extra settings required by the service handler. + + """ + self.__service_label = "P4 UPF offloading service" + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(self.__service.service_config, **settings) + + self._init_settings() + self._parse_settings() + self._print_settings() + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], + connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + """ Create/Update service endpoints from a list. + Parameters: + endpoints: List[Tuple[str, str, Optional[str]]] + List of tuples, each containing a device_uuid, + endpoint_uuid and, optionally, the topology_uuid + of the endpoint to be added. + connection_uuid : Optional[str] + If specified, is the UUID of the connection this endpoint is associated to. + Returns: + results: List[Union[bool, Exception]] + List of results for endpoint changes requested. + Return values must be in the same order as the requested + endpoints. If an endpoint is properly added, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + LOGGER.info(f"{self.__service_label} - Provision service configuration") + + visited = set() + results = [] + for endpoint in endpoints: + device_uuid, endpoint_uuid = endpoint[0:2] + device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_name = device.name + + LOGGER.info(f"Device {device_name}") + LOGGER.info(f"\t | Service endpoint UUID: {endpoint_uuid}") + + port_id = find_port_id_in_endpoint_list(device.device_endpoints, endpoint_uuid) + LOGGER.info(f"\t | Service port ID: {port_id}") + + dev_port_key = device_name + "-" + PORT_PREFIX + str(port_id) + + # Skip already visited device ports + if dev_port_key in visited: + continue + + # Skip non-dataplane ports + if port_id not in [self.__upf[UPLINK_PORT], self.__upf[DOWNLINK_PORT]]: + LOGGER.info(f"\t | Port ID {port_id} is not an UL or DP port; skipping...") + continue + + rules = [] + actual_rules = -1 + applied_rules, failed_rules = 0, -1 + + # Create and apply rules + try: + # Uplink (UL) rules + if port_id == self.__upf[UPLINK_PORT]: + rules = self._create_rules_uplink( + device_obj=device, + port_id=port_id, + next_id=self.__upf[DOWNLINK_PORT], + action=ConfigActionEnum.CONFIGACTION_SET) + # Downlink (DL) rules + elif port_id == self.__upf[DOWNLINK_PORT]: + rules = self._create_rules_downlink( + device_obj=device, + port_id=port_id, + next_id=self.__upf[UPLINK_PORT], + action=ConfigActionEnum.CONFIGACTION_SET) + + actual_rules = len(rules) + LOGGER.info(f"\t # of rules {actual_rules}") + applied_rules, failed_rules = apply_rules( + task_executor=self.__task_executor, + device_obj=device, + json_config_rules=rules + ) + except Exception as ex: + LOGGER.error(f"Failed to insert UPF rules on device {device.name} due to {ex}") + results.append(ex) + finally: + rules.clear() + + # Ensure correct status + if (failed_rules == 0) and (applied_rules == actual_rules): + LOGGER.info(f"Installed {applied_rules}/{actual_rules} UPF rules on device {device_name} and port {port_id}") + results.append(True) + + # You should no longer visit this device port again + visited.add(dev_port_key) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], + connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + """ Delete service endpoints from a list. + Parameters: + endpoints: List[Tuple[str, str, Optional[str]]] + List of tuples, each containing a device_uuid, + endpoint_uuid, and the topology_uuid of the endpoint + to be removed. + connection_uuid : Optional[str] + If specified, is the UUID of the connection this endpoint is associated to. + Returns: + results: List[Union[bool, Exception]] + List of results for endpoint deletions requested. + Return values must be in the same order as the requested + endpoints. If an endpoint is properly deleted, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + LOGGER.info(f"{self.__service_label} - Deprovision service configuration") + + visited = set() + results = [] + for endpoint in endpoints: + device_uuid, endpoint_uuid = endpoint[0:2] + device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_name = device.name + + LOGGER.info(f"Device {device_name}") + LOGGER.info(f"\t | Service endpoint UUID: {endpoint_uuid}") + + port_id = find_port_id_in_endpoint_list(device.device_endpoints, endpoint_uuid) + LOGGER.info(f"\t | Service port ID: {port_id}") + + dev_port_key = device_name + "-" + PORT_PREFIX + str(port_id) + + # Skip already visited device ports + if dev_port_key in visited: + continue + + # Skip non-dataplane ports + if port_id not in [self.__upf[UPLINK_PORT], self.__upf[DOWNLINK_PORT]]: + LOGGER.info(f"\t | Port ID {port_id} is not an UL or DP port; skipping...") + continue + + rules = [] + actual_rules = -1 + applied_rules, failed_rules = 0, -1 + + # Create and apply rules + try: + # Uplink (UL) rules + if port_id == self.__upf[UPLINK_PORT]: + rules = self._create_rules_uplink( + device_obj=device, + port_id=port_id, + next_id=self.__upf[DOWNLINK_PORT], + action=ConfigActionEnum.CONFIGACTION_DELETE) + # Downlink (DL) rules + elif port_id == self.__upf[DOWNLINK_PORT]: + rules = self._create_rules_downlink( + device_obj=device, + port_id=port_id, + next_id=self.__upf[UPLINK_PORT], + action=ConfigActionEnum.CONFIGACTION_DELETE) + + actual_rules = len(rules) + LOGGER.info(f"\t # of rules {actual_rules}") + applied_rules, failed_rules = apply_rules( + task_executor=self.__task_executor, + device_obj=device, + json_config_rules=rules + ) + except Exception as ex: + LOGGER.error(f"Failed to delete UPF rules from device {device.name} due to {ex}") + results.append(ex) + finally: + rules.clear() + + # Ensure correct status + if (failed_rules == 0) and (applied_rules == actual_rules): + LOGGER.info(f"Deleted {applied_rules}/{actual_rules} UPF rules from device {device_name} and port {port_id}") + results.append(True) + + # You should no longer visit this device port again + visited.add(dev_port_key) + + return results + + @metered_subclass_method(METRICS_POOL) + def SetConstraint(self, constraints: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ Create/Update service constraints. + Parameters: + constraints: List[Tuple[str, Any]] + List of tuples, each containing a constraint_type and the + new constraint_value to be set. + Returns: + results: List[Union[bool, Exception]] + List of results for constraint changes requested. + Return values must be in the same order as the requested + constraints. If a constraint is properly set, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConstraint(self, constraints: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ Delete service constraints. + Parameters: + constraints: List[Tuple[str, Any]] + List of tuples, each containing a constraint_type pointing + to the constraint to be deleted, and a constraint_value + containing possible additionally required values to locate + the constraint to be removed. + Returns: + results: List[Union[bool, Exception]] + List of results for constraint deletions requested. + Return values must be in the same order as the requested + constraints. If a constraint is properly deleted, True must + be returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ Create/Update configuration for a list of service resources. + Parameters: + resources: List[Tuple[str, Any]] + List of tuples, each containing a resource_key pointing to + the resource to be modified, and a resource_value + containing the new value to be set. + Returns: + results: List[Union[bool, Exception]] + List of results for resource key changes requested. + Return values must be in the same order as the requested + resource keys. If a resource is properly set, True must be + returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + msg = '[SetConfig] Method not implemented. Resources({:s}) are being ignored.' + LOGGER.warning(msg.format(str(resources))) + return [True for _ in range(len(resources))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources: List[Tuple[str, Any]]) \ + -> List[Union[bool, Exception]]: + """ Delete configuration for a list of service resources. + Parameters: + resources: List[Tuple[str, Any]] + List of tuples, each containing a resource_key pointing to + the resource to be modified, and a resource_value containing + possible additionally required values to locate the value + to be removed. + Returns: + results: List[Union[bool, Exception]] + List of results for resource key deletions requested. + Return values must be in the same order as the requested + resource keys. If a resource is properly deleted, True must + be returned; otherwise, the Exception that is raised during + the processing must be returned. + """ + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + msg = '[DeleteConfig] Method not implemented. Resources({:s}) are being ignored.' + LOGGER.warning(msg.format(str(resources))) + return [True for _ in range(len(resources))] + + def _init_settings(self): + self.__switch_info = {} + self.__port_map = {} + self.__upf = {} + self.__gnb = {} + self.__ue_map = {} + + try: + self.__settings = self.__settings_handler.get('/settings') + LOGGER.info(f"{self.__service_label} with settings: {self.__settings}") + except Exception as ex: + LOGGER.error(f"Failed to retrieve service settings: {ex}") + raise Exception(ex) + + def _parse_settings(self): + try: + switch_info = self.__settings.value[SWITCH_INFO] + except Exception as ex: + LOGGER.error(f"Failed to parse service settings: {ex}") + raise Exception(ex) + assert isinstance(switch_info, list), "Switch info object must be a list" + + for switch in switch_info: + for switch_name, sw_info in switch.items(): + assert switch_name, "Invalid P4 switch name" + assert isinstance(sw_info, dict), \ + "Switch {} info must be a map with arch, dpid, port_list, fwd_list, routing_list, upf, gnb, and ue_list items)" + assert sw_info[ARCH] in SUPPORTED_TARGET_ARCH_LIST, \ + f"Switch {switch_name} - Supported P4 architectures are: {','.join(SUPPORTED_TARGET_ARCH_LIST)}" + switch_dpid = sw_info[DPID] + assert switch_dpid > 0, f"Switch {switch_name} - P4 switch dataplane ID {sw_info[DPID]} must be a positive integer" + + # Port list + port_list = sw_info[PORT_LIST] + assert isinstance(port_list, list),\ + f"Switch {switch_name} port list must be a list with port_id, port_type, and vlan_id items" + for port in port_list: + port_id = port[PORT_ID] + assert port_id >= 0, f"Switch {switch_name} - Invalid P4 switch port ID" + port_type = port[PORT_TYPE] + assert port_type in PORT_TYPES_STR_VALID, f"Switch {switch_name} - Valid P4 switch port types are: {','.join(PORT_TYPES_STR_VALID)}" + vlan_id = port[VLAN_ID] + assert chk_vlan_id(vlan_id), f"Switch {switch_name} - Invalid VLAN ID for port {port_id}" + + if switch_name not in self.__port_map: + self.__port_map[switch_name] = {} + port_key = PORT_PREFIX + str(port_id) + if port_key not in self.__port_map[switch_name]: + self.__port_map[switch_name][port_key] = {} + self.__port_map[switch_name][port_key][PORT_ID] = port_id + self.__port_map[switch_name][port_key][PORT_TYPE] = port_type + self.__port_map[switch_name][port_key][VLAN_ID] = vlan_id + self.__port_map[switch_name][port_key][FORWARDING_LIST] = [] + self.__port_map[switch_name][port_key][ROUTING_LIST] = [] + + # Forwarding list + fwd_list = sw_info[FORWARDING_LIST] + assert isinstance(fwd_list, list), f"Switch {switch_name} forwarding list must be a list" + for fwd_entry in fwd_list: + port_id = fwd_entry[PORT_ID] + assert port_id >= 0, f"Invalid port ID: {port_id}" + host_mac = fwd_entry[HOST_MAC] + assert chk_address_mac(host_mac), f"Invalid host MAC address {host_mac}" + host_label = "" + if HOST_LABEL in fwd_entry: + host_label = fwd_entry[HOST_LABEL] + + # Retrieve entry from the port map + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + + host_facing_port = self._is_host_facing_port(switch_name, port_id) + LOGGER.info(f"Switch {switch_name} - Port {port_id}: Is host facing: {"True" if host_facing_port else "False"}") + switch_port_entry[FORWARDING_LIST].append( + { + HOST_MAC: host_mac, + HOST_LABEL: host_label + } + ) + + # Routing list + routing_list = sw_info[ROUTING_LIST] + assert isinstance(routing_list, list), f"Switch {switch_name} routing list must be a list" + for rt_entry in routing_list: + port_id = rt_entry[PORT_ID] + assert port_id >= 0, f"Invalid port ID: {port_id}" + ipv4_dst = rt_entry[IPV4_DST] + assert chk_address_ipv4(ipv4_dst), f"Invalid destination IPv4 address {ipv4_dst}" + ipv4_prefix_len = rt_entry[IPV4_PREFIX_LEN] + assert chk_prefix_len_ipv4(ipv4_prefix_len), f"Invalid IPv4 address prefix length {ipv4_prefix_len}" + mac_src = rt_entry[MAC_SRC] + assert chk_address_mac(mac_src), f"Invalid source MAC address {mac_src}" + mac_dst = rt_entry[MAC_DST] + assert chk_address_mac(mac_dst), f"Invalid destination MAC address {mac_dst}" + + # Retrieve entry from the port map + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + + # Add routing entry + switch_port_entry[ROUTING_LIST].append( + { + PORT_ID: port_id, + IPV4_DST: ipv4_dst, + IPV4_PREFIX_LEN: ipv4_prefix_len, + MAC_SRC: mac_src, + MAC_DST: mac_dst + } + ) + + # UPF + upf = sw_info[UPF] + + self.__upf[UPLINK_PORT] = upf[UPLINK_PORT] + assert self.__upf[UPLINK_PORT] >= 0, f"Invalid uplink UPF port: {self.__upf[UPLINK_PORT]}" + + self.__upf[UPLINK_IP] = upf[UPLINK_IP] + assert chk_address_ipv4(self.__upf[UPLINK_IP]), f"Invalid uplink UPF IPv4 address {self.__upf[UPLINK_IP]}" + + self.__upf[UPLINK_MAC] = upf[UPLINK_MAC] + assert chk_address_mac(self.__upf[UPLINK_MAC]), f"Invalid uplink UPF MAC address {self.__upf[UPLINK_MAC]}" + + self.__upf[DOWNLINK_PORT] = upf[DOWNLINK_PORT] + assert self.__upf[DOWNLINK_PORT] >= 0, f"Invalid downlink UPF port: {self.__upf[DOWNLINK_PORT]}" + + self.__upf[DOWNLINK_IP] = upf[DOWNLINK_IP] + assert chk_address_ipv4(self.__upf[DOWNLINK_IP]), f"Invalid downlink UPF IPv4 address {self.__upf[DOWNLINK_IP]}" + + self.__upf[DOWNLINK_MAC] = upf[DOWNLINK_MAC] + assert chk_address_mac(self.__upf[DOWNLINK_MAC]), f"Invalid downlink UPF MAC address {self.__upf[DOWNLINK_MAC]}" + + slice_id = upf[SLICE_ID] + assert slice_id >= 0, "Slice ID must be a non-negative integer" + self.__upf[SLICE_ID] = slice_id + + teid = upf[TEID] + assert teid >= 0, "TEID must be a non-negative integer" + self.__upf[TEID] = teid + + app_id = upf[APP_ID] + assert app_id >= 0, "App ID must be a non-negative integer" + self.__upf[APP_ID] = app_id + + app_meter_id = upf[APP_METER_ID] + assert app_meter_id >= 0, "App meter ID must be a non-negative integer" + self.__upf[APP_METER_ID] = app_meter_id + + ctr_id = upf[CTR_ID] + assert ctr_id >= 0, "Ctr ID must be a non-negative integer" + self.__upf[CTR_ID] = ctr_id + + tc_id = upf[TC_ID] + assert tc_id >= 0, "TC ID must be a non-negative integer" + self.__upf[TC_ID] = tc_id + + tunnel_peer_id = upf[TUNNEL_PEER_ID] + assert tunnel_peer_id >= 0, "Tunnel peer ID must be a non-negative integer" + self.__upf[TUNNEL_PEER_ID] = tunnel_peer_id + + # gNB configuration + gnb = sw_info[GNB] + self.__gnb[IP] = gnb[IP] + assert chk_address_ipv4(self.__gnb[IP]), f"Invalid 5G gNB IPv4 address {self.__gnb[IP]}" + + self.__gnb[MAC] = gnb[MAC] + assert chk_address_mac(self.__gnb[MAC]), f"Invalid 5G gNB MAC address {self.__gnb[MAC]}" + + # UE list + ue_list = sw_info[UE_LIST] + assert isinstance(ue_list, list), f"Switch {switch_name} UE list must be a list" + for ue in ue_list: + ue_id = ue[UE_ID] + assert ue_id, f"Empty UE ID: {UE_ID}" + ue_ip = ue[UE_IP] + assert chk_address_ipv4(ue_ip), "Invalid UE IPv4 address" + self.__ue_map[ue_ip] = {} + self.__ue_map[ue_ip][UE_ID] = ue_id + self.__ue_map[ue_ip][UE_IP] = ue_ip + + # PDU list per UE + self.__ue_map[ue_ip][PDU_LIST] = [] + pdu_session_list = ue[PDU_LIST] + assert isinstance(pdu_session_list, list), f"UE {ue_id} PDU session list must be a list" + for pdu in pdu_session_list: + pdu_id = pdu[PDU_SESSION_ID] + assert pdu_id >= 0, "PDU ID must be a non-negative integer" + assert pdu_id == DEF_SESSION_METER_ID, "Better use PDU session ID = 0, as only this is supported for now" + dnn = pdu[DNN] + assert dnn, "Data network name is invalid" + session_type_str = pdu[PDU_SESSION_TYPE] + assert session_type_str == ETHER_TYPE_IPV4, f"Only supported PDU session type for now is {ETHER_TYPE_IPV4}" + gtpu_tunnel = pdu[GTPU_TUNNEL] + assert isinstance(gtpu_tunnel, dict), "GTP-U tunnel info must be a map with uplink and downlink items)" + + gtpu_ul = gtpu_tunnel[UPLINK] + assert isinstance(gtpu_ul, dict), "GTP-U tunnel UL info must be a map with src and dst IP items)" + assert chk_address_ipv4(gtpu_ul[SRC]), f"Invalid GTP-U UL src IPv4 address {gtpu_ul[SRC]}" + assert chk_address_ipv4(gtpu_ul[DST]), f"Invalid GTP-U UL dst IPv4 address {gtpu_ul[DST]}" + + gtpu_dl = gtpu_tunnel[DOWNLINK] + assert isinstance(gtpu_dl, dict), "GTP-U tunnel DL info must be a map with src and dst IP items)" + assert chk_address_ipv4(gtpu_dl[SRC]), f"Invalid GTP-U DL src IPv4 address {gtpu_dl[SRC]}" + assert chk_address_ipv4(gtpu_dl[DST]), f"Invalid GTP-U DL dst IPv4 address {gtpu_dl[DST]}" + + self.__ue_map[ue_ip][PDU_LIST].append(pdu) + + # QoS flows per UE + self.__ue_map[ue_ip][QOS_FLOWS] = [] + qos_flows_list = ue[QOS_FLOWS] + assert isinstance(qos_flows_list, list), f"UE {ue_id} QoS flows' list must be a list" + for flow in qos_flows_list: + qfi = flow[QFI] + assert qfi >= 0, "QFI must be a non-negative integer" + fiveqi = flow[FIVEQI] + assert fiveqi >= 0, "5QI must be a non-negative integer" + qos_type = flow[QOS_TYPE] + assert qos_type.casefold() in (s.casefold() for s in QOS_TYPES_STR_VALID), \ + f"UE {ue_id} - Valid QoS types are: {','.join(QOS_TYPES_STR_VALID)}" + self.__ue_map[ue_ip][QOS_FLOWS].append(flow) + + self.__switch_info[switch_name] = sw_info + + def _print_settings(self): + LOGGER.info(f"--------------- {self.__service.name} settings ---------------") + LOGGER.info("--- Topology info") + for switch_name, switch_info in self.__switch_info.items(): + LOGGER.info(f"\t Device {switch_name}") + LOGGER.info(f"\t\t| Target P4 architecture: {switch_info[ARCH]}") + LOGGER.info(f"\t\t| Data plane ID: {switch_info[DPID]}") + LOGGER.info("\t\t| 5G UPF Configuration:") + LOGGER.info(f"\t\t\t| Uplink Port: {self.__upf[UPLINK_PORT]}") + LOGGER.info(f"\t\t\t| Uplink IP: {self.__upf[UPLINK_IP]}") + LOGGER.info(f"\t\t\t| Uplink MAC: {self.__upf[UPLINK_MAC]}") + LOGGER.info(f"\t\t\t| Downlink Port: {self.__upf[DOWNLINK_PORT]}") + LOGGER.info(f"\t\t\t| Downlink IP: {self.__upf[DOWNLINK_IP]}") + LOGGER.info(f"\t\t\t| Downlink MAC: {self.__upf[DOWNLINK_MAC]}") + LOGGER.info(f"\t\t\t| Slice ID: {self.__upf[SLICE_ID]}") + LOGGER.info(f"\t\t\t| Tunnel Endpoint ID: {self.__upf[TEID]}") + LOGGER.info(f"\t\t\t| App ID: {self.__upf[APP_ID]}") + LOGGER.info(f"\t\t\t| App Meter ID: {self.__upf[APP_METER_ID]}") + LOGGER.info(f"\t\t\t| Ctr ID: {self.__upf[CTR_ID]}") + LOGGER.info(f"\t\t\t| TC ID: {self.__upf[TC_ID]}") + LOGGER.info(f"\t\t\t| Tunnel Peer ID: {self.__upf[TUNNEL_PEER_ID]}\n") + # LOGGER.info("\n") + LOGGER.info("\t\t| 5G gNB Configuration:") + LOGGER.info(f"\t\t\t| 5G gNB IP: {self.__gnb[IP]}") + LOGGER.info(f"\t\t\t| 5G gNB MAC: {self.__gnb[MAC]}\n") + LOGGER.info("\t\t| Port Map:") + for _, port_map in self.__port_map[switch_name].items(): + LOGGER.info(f"\t\t\t| Port ID: {port_map[PORT_ID]}") + LOGGER.info(f"\t\t\t| Port type: {port_map[PORT_TYPE]}") + LOGGER.info(f"\t\t\t| Port VLAN ID: {port_map[VLAN_ID]}") + LOGGER.info(f"\t\t\t| FWD list: {port_map[FORWARDING_LIST]}") + LOGGER.info(f"\t\t\t| Routing list: {port_map[ROUTING_LIST]}\n") + LOGGER.info("\t\t| UE List:") + for ue_key, ue_info in self.__ue_map.items(): + assert ue_key == ue_info[UE_IP], "UE key is not the UE IPv4 address" + ue_ip = ue_info[UE_IP] + LOGGER.info(f"\t\t\t| UE ID: {ue_info[UE_ID]}") + LOGGER.info(f"\t\t\t| UE IP: {ue_info[UE_IP]}") + for pdu in self.__ue_map[ue_ip][PDU_LIST]: + LOGGER.info(f"\t\t\t\t| PDU session ID: {pdu[PDU_SESSION_ID]}") + LOGGER.info(f"\t\t\t\t| DNN: {pdu[DNN]}") + LOGGER.info(f"\t\t\t\t| PDU session type: {pdu[PDU_SESSION_TYPE]}") + LOGGER.info(f"\t\t\t\t| GTP-U tunnel UL Src. IP: {pdu[GTPU_TUNNEL][UPLINK][SRC]}") + LOGGER.info(f"\t\t\t\t| GTP-U tunnel UL Dst. IP: {pdu[GTPU_TUNNEL][UPLINK][DST]}") + LOGGER.info(f"\t\t\t\t| GTP-U tunnel DL Src. IP: {pdu[GTPU_TUNNEL][DOWNLINK][SRC]}") + LOGGER.info(f"\t\t\t\t| GTP-U tunnel DL Dst. IP: {pdu[GTPU_TUNNEL][DOWNLINK][DST]}\n") + for flow in self.__ue_map[ue_ip][QOS_FLOWS]: + LOGGER.info(f"\t\t\t\t| QoS QFI: {flow[QFI]}") + LOGGER.info(f"\t\t\t\t| QoS 5QI: {flow[FIVEQI]}") + LOGGER.info(f"\t\t\t\t| QoS Type: {flow[QOS_TYPE]}\n") + LOGGER.info("-------------------------------------------------------") + + def _get_switch_port_in_port_map(self, switch_name : str, port_id : int) -> Dict: + assert switch_name, "A valid switch name must be used as a key to the port map" + assert port_id > 0, "A valid switch port ID must be used as a key to a switch's port map" + switch_entry = self.__port_map[switch_name] + assert switch_entry, f"Switch {switch_name} does not exist in the port map" + port_key = PORT_PREFIX + str(port_id) + assert switch_entry[port_key], f"Port with ID {port_id} does not exist in the switch map" + + return switch_entry[port_key] + + def _get_port_type_of_switch_port(self, switch_name : str, port_id : int) -> str: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[PORT_TYPE] + + def _get_vlan_id_of_switch_port(self, switch_name : str, port_id : int) -> int: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[VLAN_ID] + + def _get_fwd_list_of_switch_port(self, switch_name : str, port_id : int) -> List [Tuple]: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[FORWARDING_LIST] + + def _get_routing_list_of_switch_port(self, switch_name : str, port_id : int) -> List [Tuple]: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[ROUTING_LIST] + + def _is_host_facing_port(self, switch_name : str, port_id : int) -> bool: + return self._get_port_type_of_switch_port(switch_name, port_id) == PORT_TYPE_HOST + + def _create_rules_uplink( + self, + device_obj : Device, # type: ignore + port_id : int, + next_id : int, + action : ConfigActionEnum): # type: ignore + dev_name = device_obj.name + vlan_id = self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id) + + rules = [] + + # Port setup rules + try: + rules += rules_set_up_port( + port=port_id, + port_type=PORT_TYPE_HOST, + fwd_type=FORWARDING_TYPE_BRIDGING, + vlan_id=vlan_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating port setup rules") + raise Exception(ex) + + ### UPF rules + try: + rules += rules_set_up_upf_interfaces( + ipv4_dst=self.__upf[UPLINK_IP], # UPF's N3 interface + ipv4_prefix_len=32, + gtpu_value=GTPU_VALID, + slice_id=self.__upf[SLICE_ID], + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF interface rule") + raise Exception(ex) + + try: + rules += rules_set_up_upf_uplink_sessions( + tun_ip_address=self.__upf[UPLINK_IP], # UPF's N3 interface + teid=self.__upf[TEID], + session_meter_id=DEF_SESSION_METER_ID, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF UL session rule") + raise Exception(ex) + + # UE-specific rules + for _, ue_info in self.__ue_map.items(): + try: + rules += rules_set_up_upf_uplink_terminations( + ue_session_id=ue_info[UE_IP], # UE's IPv4 address + app_id=self.__upf[APP_ID], + ctr_id=self.__upf[CTR_ID], + app_meter_id=self.__upf[APP_METER_ID], + tc_id=self.__upf[TC_ID], + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF termination rule") + raise Exception(ex) + + # L2 Forwarding rules + fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) + for host_map in fwd_list: + mac_dst = host_map[HOST_MAC] + label = host_map[HOST_LABEL] + LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") + try: + ### Bridging rules + rules += rules_set_up_fwd_bridging( + vlan_id=vlan_id, + eth_dst=mac_dst, + egress_port=port_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating bridging rule") + raise Exception(ex) + + try: + ### Pre-next VLAN rule + rules += rules_set_up_pre_next_vlan( + next_id=next_id, + vlan_id=vlan_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating pre-next VLAN rule") + raise Exception(ex) + + ### Static routing rules + routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=port_id) + for rt_entry in routing_list: + LOGGER.info(f"Creating routing rule for dst IP {rt_entry[IPV4_DST]}/{rt_entry[IPV4_PREFIX_LEN]} with src MAC {rt_entry[MAC_SRC]}, dst MAC {rt_entry[MAC_DST]}") + + try: + ### Next profile for hashed routing + rules += rules_set_up_next_profile_hashed_routing( + egress_port=port_id, + next_id=next_id, + eth_src=rt_entry[MAC_SRC], # UPF's N6 interface (self.__upf[DOWNLINK_MAC]) + eth_dst=rt_entry[MAC_DST], # Data network's N6 interface + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating next profile for hashed routing") + raise Exception(ex) + + try: + ### Next hashed for routing + rules += rules_set_up_next_hashed( + egress_port=port_id, + next_id=next_id, + action=action + ) + ### Route towards destination + rules += rules_set_up_routing( + ipv4_dst=rt_entry[IPV4_DST], + ipv4_prefix_len=rt_entry[IPV4_PREFIX_LEN], + egress_port=port_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating static L3 routing rules") + raise Exception(ex) + + return rules + + def _create_rules_downlink( + self, + device_obj : Device, # type: ignore + port_id : int, + next_id : int, + action : ConfigActionEnum): # type: ignore + dev_name = device_obj.name + vlan_id = self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id) + + rules = [] + + # Port setup + try: + rules += rules_set_up_port( + port=port_id, + port_type=PORT_TYPE_HOST, + fwd_type=FORWARDING_TYPE_BRIDGING, + vlan_id=vlan_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating port setup rules") + raise Exception(ex) + + ### UPF + for _, ue_info in self.__ue_map.items(): + try: + rules += rules_set_up_upf_interfaces( + ipv4_dst=ue_info[UE_IP], + ipv4_prefix_len=32, + gtpu_value=GTPU_INVALID, + slice_id=self.__upf[SLICE_ID], + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF interface rule") + raise Exception(ex) + + try: + rules += rules_set_up_upf_downlink_sessions( + ipv4_dst=ue_info[UE_IP], + session_meter_id=ue_info[PDU_LIST][0][PDU_SESSION_ID], # Should match DEF_SESSION_METER_ID + tun_peer_id=self.__upf[TUNNEL_PEER_ID], + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF DL session rule") + raise Exception(ex) + + try: + rules += rules_set_up_upf_downlink_terminations( + ue_session_id=ue_info[UE_IP], # UE's IPv4 address + app_id=self.__upf[APP_ID], + ctr_id=self.__upf[CTR_ID], + app_meter_id=self.__upf[APP_METER_ID], + tc_id=self.__upf[TC_ID], + teid=self.__upf[TEID], + qfi=ue_info[QOS_FLOWS][0][QFI], + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF DL termination rule") + raise Exception(ex) + + try: + rules += rules_set_up_upf_downlink_ig_tunnel_peers( + tun_peer_id=self.__upf[TUNNEL_PEER_ID], + tun_dst_addr=self.__upf[UPLINK_IP], # UPF's N3 interface + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF DL ingress tunnel peers rule") + raise Exception(ex) + + try: + rules += rules_set_up_upf_downlink_eg_tunnel_peers( + tun_peer_id=self.__upf[TUNNEL_PEER_ID], + tun_src_addr=self.__upf[UPLINK_IP], # UPF's N3 interface + tun_dst_addr=self.__gnb[IP], # gNB's N3 interface + tun_src_port=GTP_PORT, # GTP-U port + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating UPF DL egress tunnel peers rule") + raise Exception(ex) + + # L2 Forwarding + fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) + for host_map in fwd_list: + mac_dst = host_map[HOST_MAC] + label = host_map[HOST_LABEL] + LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") + try: + ### Bridging + rules += rules_set_up_fwd_bridging( + vlan_id=vlan_id, + eth_dst=mac_dst, + egress_port=port_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating bridging rule") + raise Exception(ex) + + try: + ### Pre-next VLAN + rules += rules_set_up_pre_next_vlan( + next_id=next_id, + vlan_id=vlan_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating pre-next VLAN rule") + raise Exception(ex) + + ### Static routing + routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=port_id) + for rt_entry in routing_list: + try: + ### Next profile for hashed routing + rules += rules_set_up_next_profile_hashed_routing( + egress_port=port_id, + next_id=next_id, + eth_src=rt_entry[MAC_SRC], # UPF's N3 interface (self.__upf[UPLINK_MAC]) + eth_dst=rt_entry[MAC_DST], # gNB's N3 interface (self.__gnb[MAC]) + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating next profile for hashed routing") + raise Exception(ex) + + try: + ### Next hashed for routing + rules += rules_set_up_next_hashed( + egress_port=port_id, + next_id=next_id, + action=action + ) + ### Route towards destination + rules += rules_set_up_routing( + ipv4_dst=rt_entry[IPV4_DST], + ipv4_prefix_len=rt_entry[IPV4_PREFIX_LEN], + egress_port=port_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating static L3 routing rules") + raise Exception(ex) + + return rules + diff --git a/src/webui/service/static/topology_icons/oran-cn.png b/src/webui/service/static/topology_icons/oran-cn.png new file mode 100644 index 0000000000000000000000000000000000000000..f1555434f9db8e4268b6544933069601bb79eeab GIT binary patch literal 23796 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 O3=E#GelF{r5}E*D1k-K+ literal 0 HcmV?d00001 diff --git a/src/webui/service/static/topology_icons/oran-cu.png b/src/webui/service/static/topology_icons/oran-cu.png new file mode 100644 index 0000000000000000000000000000000000000000..aff6986e3800b4184d942f9a8edb24a406db1ccc GIT binary patch literal 26326 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 FO#n*LaV-D< literal 0 HcmV?d00001 diff --git a/src/webui/service/static/topology_icons/oran-du.png b/src/webui/service/static/topology_icons/oran-du.png new file mode 100644 index 0000000000000000000000000000000000000000..d486305cf02b0fe1ed6c92f38ce1cb21887cd3bc GIT binary patch literal 26185 zcmeAS@N?(olHy`uVBq!ia0y~yVBE*Rz}U;d#=yXkmn`y%fq{XsILO_JVcj{ImkbOH zEa{HEjtmSN`?>!lvNA9*a29w(7Besy9ROiQjg+X{3=FCFJY5_^D&pSGWv>Yd{dRo) zx&=v$m(ql)yq(_bvh2?OxqDjZm91->jPe;=#d5w}ShIR{_CdXlsk18e8C_UhGQY)z zEiGT#@x|=p)9gf-mFRhvP@9Da{f2r%t*q`m5b>Pt#Ob}-yV&;SmF)iC?RNO@eA9|oFHW%)_pe>?{prS~k@IE#9{Boh zqwBhr?cz&%t$yXrdj4R}mDT)NTi4q~#Qc6$yE*&+-wW1Ttr$BdzbKVxOO#Mk{84}2 z^7V&h*Y^I2j*fc~e|?krnzQ@<-*`1Acf)Gc1NoK=vo|xEnXpG(=6UdZ8H2qE+mBU> z2hzVX+O1^YV8oqJsdk`9{J^G?hW;Q%I}gT)2=;_PSqqE5J?Fpguf4UF{hr0wKg{dr z|GLhc-?hwytu0Zaty43j?BCnByMNZdzj5?`_3P@soUB?FzAel*ZnGs+XE&7YWZ0j= z{Aa!80sgZG{^tsRsK3kdC*FkZe>(5M|AovJ+xFPWL`U)W)lJ}Z7vK5t#Ba%hlsYMy zd9#_?yf_xF7i`%q5pXAxC*XEx9z?SbuUIcQCKTmc_N%|L2wE{@H!&E^oUPv&?Ij4eOZ?Y}>{#JBm3c zgL8wKctR=P0exMDx-7nb?`O01tlyYeGk==#zWz<8B;&1QD)o8!&U-f=URwHKLf5f! zF_}4)Y%&$g#bhd8hupB=y6pcA>E#T2N`(%XFjmhKKmX6r)8y{=O(zy@H1cuM(6WlG z@P5_VbW^uAd~ZZW?dcYGTP3zBO)uiP4y}GGB=Ziv1h^%1Zw%FMYK{dhUMi0}szJSX(pYP4{cwdwxDw^}Mx=^>$yy|4$U>Wxn%!PejEF*Nam3 zL^4hOEqTfpKP@h2&(Yqc;>RpA-rf8nHi_@)W~oEQzt=l`tW{{MVR>|pRe_C#S3%*= zf9B_n5?|~(>T5X;Exx--{F0KzOta@4Tp`n6nD1U5u;uEnzdSYO1^Z_e?q`11u;a6> zK=bqljVpH3ZoTLho3AADHE3Z*yu^2-^>^N{z4GJzZn0*KFK-3A!fLa8OP$R3*jq3E z`MqR^!~*F96VEZ!m)NYEE6cELyM11bH1o0TYT^&B=N$e~mlJmJhn;!P`xl?s3cfT~ z6{IAJ$yDhJfAcRt_$^THhTSFB{rv?8-ye?oaJyFW!|lD28*($)a-<)%0qmwyryU*pUx2v7>|G<*TjLYWq{Quv5ce2a5bK)+!Rra2TbB~rD(G5N? z(6!C;zw(hsTD#vxu{d3vCo*s6*&_>fzFhQvOUcFbU-2*2TFR|A3%axSj>fmDhy2dv zd$^Nc7zr?Y=`T9#$^Wfy&nvdS-?P7@Idk&{1l9}H{yOn<_wjiv7rnbsuWs_~lTqD% z_oHX8K1f`*i{a1=$CvkSzT|3H$G~nCYO=@ZaW?;l>gtBIxr}D>Z~RcOxbibK+2Wh0 zv&F78jBnW&+?3xv9M z`O05fQ)90#R>!vIR@SX4A{AS&A6?KoHQ6kbvxBLWKewq+OXI!%H>s zoLvWbXXh6jTvk`Q?RR>Y+m|@m&PDHR|NT?(Z2A89qQjTZl`q~}F{OUfb^5s0yldyl zzmbA`+|^gwkF4Ds{4#Y3b9ME^KC1`sV=nS>_M7#Z%{JKcYIfDX%ry&_tZ;W(_$$BG zN%*nPpZXFRmgB2lJe_*|o3La&-+^5@76$7L-_-4sdsC;z{jWkxTk+WTjs4PQaj)BY zukYZ?ds}|&`hL?ywvTW9Hhj$8b&L1)`l6rL<0tN%eQ&pM!{jf^PkXdY zougE{ZLuB8w>dA2_o|(3m{QU8Ua)K1=}RfoR<#(j+wPNhvA=KmrRdUw!1}Hav1$|V zeUA2@-EVEaf1ajDY2%LMeV#p`9vAyIezfdcG`ar%jgOu;E(<1{=TzG`W96A21#aol z$w{ZTg%vDVV)$n6C5CT%LgdTr)%!o@?zzP)y{_c&vi+%tcAdUsUH5LGhU60kLT4MMXuRqc{3hhYU0o~pR;|i1GvLbIz^~t$jIRBZ73=z?U)8x| zQHD6z`fpn|)*Mth7m)P0SnMqK`L+LF>(9MsvuW{S`EOAMwjn=FYb?&}+{-JYk;_ya zw|4#Pd-L_LwBJ2{I=x1E-HDd7)6);_^1gGsZlB3gyR)5pLWCbXFZ{Vsk{)azNj-`Oa9Ftljbbcf0ll}YJGeD z=UM0KZlovWgmU)sr4Q^Y7`lMSFAhEZ=mIp@~bcB7DofFPnlA!u!|zsO~U6 zCH}>zaB=-dRj0t4KPJsQIazvc*>~@~_kaC)op-IxTw5YOLG$psZv_Q)!8d+KF}q9N z|K)vW!`n$Z5#^50Ud{h}_oa#Zr@*s4{H@OOTH+__hrmaga2 z;XG3^^~{f4b32pYmI_?y@e?QX^4|C{Y5IeUcFN11*l+%_InTZS-7A(2w~t&oB&Zkn zf3dPm%mtBeez^}k9!Hl4s{N@e6kB6^{OjXs^Ym&rXIEbT?V4G0!}y^1_Y!we?L+gA zA3Yu+qcrzeUTFD??z_`3<*3NqS$K5y+YO~b+@jASHUIf(YsT*L_^OSK;#h&rLcJ zv)}(Js_marRkd!3$$pI<|N8i4xx!smp%vcqgyv03SGrK&8a`WfνcKlTaTOWObA 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 literal 0 HcmV?d00001 -- GitLab From 9099c938b16d503a228fdf0e1d71b6c70efab562 Mon Sep 17 00:00:00 2001 From: "Georgios P. Katsikas" Date: Tue, 20 Jan 2026 01:06:55 +0200 Subject: [PATCH 2/3] fix: P4 UPF handler messages --- .../p4_fabric_tna_upf_service_handler.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py index bb4afec97..c378a0738 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py @@ -112,6 +112,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): rules = [] actual_rules = -1 applied_rules, failed_rules = 0, -1 + label = "" # Create and apply rules try: @@ -122,6 +123,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): port_id=port_id, next_id=self.__upf[DOWNLINK_PORT], action=ConfigActionEnum.CONFIGACTION_SET) + label = "uplink (UL)" # Downlink (DL) rules elif port_id == self.__upf[DOWNLINK_PORT]: rules = self._create_rules_downlink( @@ -129,23 +131,22 @@ class P4FabricUPFServiceHandler(_ServiceHandler): port_id=port_id, next_id=self.__upf[UPLINK_PORT], action=ConfigActionEnum.CONFIGACTION_SET) - + label = "downlink (DL)" actual_rules = len(rules) - LOGGER.info(f"\t # of rules {actual_rules}") applied_rules, failed_rules = apply_rules( task_executor=self.__task_executor, device_obj=device, json_config_rules=rules ) except Exception as ex: - LOGGER.error(f"Failed to insert UPF rules on device {device.name} due to {ex}") + LOGGER.error(f"Failed to insert {label} UPF rules on device {device.name} due to {ex}") results.append(ex) finally: rules.clear() # Ensure correct status if (failed_rules == 0) and (applied_rules == actual_rules): - LOGGER.info(f"Installed {applied_rules}/{actual_rules} UPF rules on device {device_name} and port {port_id}") + LOGGER.info(f"Installed {applied_rules}/{actual_rules} {label} UPF rules on device {device_name} and port {port_id}") results.append(True) # You should no longer visit this device port again @@ -206,6 +207,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): rules = [] actual_rules = -1 applied_rules, failed_rules = 0, -1 + label = "" # Create and apply rules try: @@ -216,6 +218,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): port_id=port_id, next_id=self.__upf[DOWNLINK_PORT], action=ConfigActionEnum.CONFIGACTION_DELETE) + label = "uplink (UL)" # Downlink (DL) rules elif port_id == self.__upf[DOWNLINK_PORT]: rules = self._create_rules_downlink( @@ -223,23 +226,22 @@ class P4FabricUPFServiceHandler(_ServiceHandler): port_id=port_id, next_id=self.__upf[UPLINK_PORT], action=ConfigActionEnum.CONFIGACTION_DELETE) - + label = "downlink (DL)" actual_rules = len(rules) - LOGGER.info(f"\t # of rules {actual_rules}") applied_rules, failed_rules = apply_rules( task_executor=self.__task_executor, device_obj=device, json_config_rules=rules ) except Exception as ex: - LOGGER.error(f"Failed to delete UPF rules from device {device.name} due to {ex}") + LOGGER.error(f"Failed to delete {label} UPF rules from device {device.name} due to {ex}") results.append(ex) finally: rules.clear() # Ensure correct status if (failed_rules == 0) and (applied_rules == actual_rules): - LOGGER.info(f"Deleted {applied_rules}/{actual_rules} UPF rules from device {device_name} and port {port_id}") + LOGGER.info(f"Deleted {applied_rules}/{actual_rules} {label} UPF rules from device {device_name} and port {port_id}") results.append(True) # You should no longer visit this device port again @@ -713,7 +715,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): for host_map in fwd_list: mac_dst = host_map[HOST_MAC] label = host_map[HOST_LABEL] - LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") + LOGGER.info(f"\t | Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") try: ### Bridging rules rules += rules_set_up_fwd_bridging( @@ -740,7 +742,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): ### Static routing rules routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=port_id) for rt_entry in routing_list: - LOGGER.info(f"Creating routing rule for dst IP {rt_entry[IPV4_DST]}/{rt_entry[IPV4_PREFIX_LEN]} with src MAC {rt_entry[MAC_SRC]}, dst MAC {rt_entry[MAC_DST]}") + LOGGER.info(f"\t | Switch {dev_name} - Port {port_id} - Route to dst {rt_entry[IPV4_DST]}/{rt_entry[IPV4_PREFIX_LEN]}, with MAC src {rt_entry[MAC_SRC]} and dst {rt_entry[MAC_DST]}") try: ### Next profile for hashed routing @@ -866,7 +868,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): for host_map in fwd_list: mac_dst = host_map[HOST_MAC] label = host_map[HOST_LABEL] - LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") + LOGGER.info(f"\t | Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac_dst} - label: {label}") try: ### Bridging rules += rules_set_up_fwd_bridging( @@ -893,6 +895,8 @@ class P4FabricUPFServiceHandler(_ServiceHandler): ### Static routing routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=port_id) for rt_entry in routing_list: + LOGGER.info(f"\t | Switch {dev_name} - Port {port_id} - Route to dst {rt_entry[IPV4_DST]}/{rt_entry[IPV4_PREFIX_LEN]}, with MAC src {rt_entry[MAC_SRC]} and dst {rt_entry[MAC_DST]}") + try: ### Next profile for hashed routing rules += rules_set_up_next_profile_hashed_routing( -- GitLab From 9dfff1bea83bfb3adf67a5aee0d26e85fe8d9915 Mon Sep 17 00:00:00 2001 From: "Georgios P. Katsikas" Date: Thu, 26 Feb 2026 17:19:21 +0200 Subject: [PATCH 3/3] feat: upgraded SD-Fabric library with 5G UPF support --- .../p4_fabric_tna_acl_service_handler.py | 8 +- .../p4_fabric_tna_commons.py | 209 ++++++++++++++---- .../p4_fabric_tna_int_config.py | 2 +- .../p4_fabric_tna_int_service_handler.py | 46 ++-- .../p4_fabric_tna_l2_simple_config.py | 62 ------ ...p4_fabric_tna_l2_simple_service_handler.py | 52 +++-- .../p4_fabric_tna_l3_service_handler.py | 79 ++++++- .../p4_fabric_tna_upf_config.py | 66 +++++- .../p4_fabric_tna_upf_service_handler.py | 42 ++-- .../collectors/int_collector/INTCollector.py | 2 - 10 files changed, 406 insertions(+), 162 deletions(-) delete mode 100644 src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py diff --git a/src/service/service/service_handlers/p4_fabric_tna_acl/p4_fabric_tna_acl_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_acl/p4_fabric_tna_acl_service_handler.py index 0b09825da..466f37601 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_acl/p4_fabric_tna_acl_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_acl/p4_fabric_tna_acl_service_handler.py @@ -448,7 +448,7 @@ class P4FabricACLServiceHandler(_ServiceHandler): if IPV4_SRC in acl_entry: try: rules += rules_set_up_acl_filter_host( - ingress_port=port_id, + port_id=port_id, ip_address=acl_entry[IPV4_SRC], prefix_len=acl_entry[IPV4_PREFIX_LEN], ip_direction="src", @@ -461,7 +461,7 @@ class P4FabricACLServiceHandler(_ServiceHandler): if IPV4_DST in acl_entry: try: rules += rules_set_up_acl_filter_host( - ingress_port=port_id, + port_id=port_id, ip_address=acl_entry[IPV4_DST], prefix_len=acl_entry[IPV4_PREFIX_LEN], ip_direction="dst", @@ -474,7 +474,7 @@ class P4FabricACLServiceHandler(_ServiceHandler): if TRN_PORT_SRC in acl_entry: try: rules += rules_set_up_acl_filter_port( - ingress_port=port_id, + port_id=port_id, transport_port=acl_entry[TRN_PORT_SRC], transport_direction="src", action=action @@ -486,7 +486,7 @@ class P4FabricACLServiceHandler(_ServiceHandler): if TRN_PORT_DST in acl_entry: try: rules += rules_set_up_acl_filter_port( - ingress_port=port_id, + port_id=port_id, transport_port=acl_entry[TRN_PORT_DST], transport_direction="dst", action=action 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 index 08f0536c2..b03cfc908 100644 --- 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 @@ -210,11 +210,11 @@ def find_port_id_in_endpoint_list(endpoint_list : List, target_endpoint_uuid : s ################################### def rules_set_up_port_ingress( - ingress_port : int, + port_id : 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_id >= 0, "Invalid port ID 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" @@ -226,6 +226,12 @@ def rules_set_up_port_ingress( 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" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure ingress port") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== VLAN is valid {vlan_is_valid}") + LOGGER.info(f"================== Port type {port_type_int}") + rules_filtering_vlan_ingress = [] rules_filtering_vlan_ingress.append( json_config_rule( @@ -236,7 +242,7 @@ def rules_set_up_port_ingress( 'match-fields': [ { 'match-field': 'ig_port', - 'match-value': str(ingress_port) + 'match-value': str(port_id) }, { 'match-field': 'vlan_is_valid', @@ -268,6 +274,11 @@ def rules_set_up_port_egress( assert egress_port >= 0, "Invalid egress port to configure egress vlan" assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure egress vlan" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure egress port") + LOGGER.info(f"================== Port ID {egress_port}") + LOGGER.info(f"================== VLAN ID {vlan_id}") + rule_no = cache_rule(TABLE_EGRESS_VLAN, action) rules_vlan_egress = [] @@ -296,13 +307,19 @@ def rules_set_up_port_egress( return rules_vlan_egress def rules_set_up_fwd_classifier( - ingress_port : int, + port_id : 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 port_id >= 0, "Invalid port ID to configure forwarding classifier" assert fwd_type in FORWARDING_TYPES_VALID, "Invalid forwarding type to configure forwarding classifier" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure FWD classifier") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Ethernet type {eth_type}") + LOGGER.info(f"================== FWD type {fwd_type}") + rule_no = cache_rule(TABLE_FWD_CLASSIFIER, action) rules_filtering_fwd_classifier = [] @@ -315,7 +332,7 @@ def rules_set_up_fwd_classifier( 'match-fields': [ { 'match-field': 'ig_port', - 'match-value': str(ingress_port) + 'match-value': str(port_id) }, { 'match-field': 'ip_eth_type', @@ -337,7 +354,7 @@ def rules_set_up_fwd_classifier( return rules_filtering_fwd_classifier def rules_set_up_port( - port : int, + port_id : int, port_type : str, fwd_type : int, vlan_id : int, @@ -347,7 +364,7 @@ def rules_set_up_port( rules_list.extend( rules_set_up_port_ingress( - ingress_port=port, + port_id=port_id, port_type=port_type, vlan_id=vlan_id, action=action @@ -355,7 +372,7 @@ def rules_set_up_port( ) rules_list.extend( rules_set_up_fwd_classifier( - ingress_port=port, + port_id=port_id, fwd_type=fwd_type, eth_type=eth_type, action=action @@ -363,15 +380,51 @@ def rules_set_up_port( ) rules_list.extend( rules_set_up_port_egress( - egress_port=port, + egress_port=port_id, vlan_id=vlan_id, action=action ) ) - LOGGER.debug(f"Port configured:{port}") + LOGGER.debug(f"Port configured:{port_id}") return rules_list +def rules_set_up_port_host( + port : int, + vlan_id : int, + action : ConfigActionEnum, # type: ignore + fwd_type=FORWARDING_TYPE_BRIDGING, + eth_type=ETHER_TYPE_IPV4): + # This is a host facing port + port_type = PORT_TYPE_HOST + + return rules_set_up_port( + port_id=port, + port_type=port_type, + fwd_type=fwd_type, + vlan_id=vlan_id, + action=action, + eth_type=eth_type + ) + +def rules_set_up_port_switch( + port : int, + vlan_id : int, + action : ConfigActionEnum, # type: ignore + fwd_type=FORWARDING_TYPE_BRIDGING, + eth_type=ETHER_TYPE_IPV4): + # This is a switch facing port + port_type = PORT_TYPE_SWITCH + + return rules_set_up_port( + port_id=port, + port_type=port_type, + fwd_type=fwd_type, + vlan_id=vlan_id, + action=action, + eth_type=eth_type + ) + ################################### ### A. End of port setup ################################### @@ -382,13 +435,22 @@ def rules_set_up_port( ################################### def rules_set_up_fwd_bridging( + port_id : int, vlan_id: int, eth_dst : str, - egress_port : int, + next_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert port_id >= 0, "Invalid port ID to configure bridging" 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" + assert next_id >= 0, "Invalid next ID to configure bridging" + + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure bridging") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== VLAN ID {vlan_id}") + LOGGER.info(f"================== Ethernet Dst {eth_dst}") + LOGGER.info(f"================== Next ID {next_id}") rule_no = cache_rule(TABLE_BRIDGING, action) @@ -413,7 +475,7 @@ def rules_set_up_fwd_bridging( 'action-params': [ { 'action-param': 'next_id', - 'action-value': str(egress_port) + 'action-value': str(next_id) } ], 'priority': 1 @@ -424,12 +486,20 @@ def rules_set_up_fwd_bridging( return rules_fwd_bridging def rules_set_up_pre_next_vlan( + port_id : int, next_id : int, vlan_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert port_id >= 0, "Invalid port ID to configure pre-next VLAN" assert next_id >= 0, "Invalid next ID to configure pre-next VLAN" assert chk_vlan_id(vlan_id), "Invalid VLAN ID to configure pre-next VLAN" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure pre-next VLAN") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Next ID {next_id}") + LOGGER.info(f"================== VLAN ID {vlan_id}") + rule_no = cache_rule(TABLE_PRE_NEXT_VLAN, action) rules_pre_next_vlan = [] @@ -460,12 +530,17 @@ def rules_set_up_pre_next_vlan( return rules_pre_next_vlan def rules_set_up_next_profile_hashed_output( - egress_port : int, + port_id : int, next_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert egress_port >= 0, "Invalid outport to configure next profile for hashed output" + assert port_id >= 0, "Invalid port ID to configure next profile for hashed output" assert next_id >=0, "Invalid next ID to configure next profile for hashed output" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure next hashed profile for output") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Next ID {next_id}") + rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) rules_next_profile_hashed_out = [] @@ -480,7 +555,7 @@ def rules_set_up_next_profile_hashed_output( 'action-params': [ { 'action-param': 'port_num', - 'action-value': str(egress_port) + 'action-value': str(next_id) } ] } @@ -490,9 +565,16 @@ def rules_set_up_next_profile_hashed_output( return rules_next_profile_hashed_out def rules_set_up_next_output_simple( - egress_port : int, + port_id : int, + next_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert egress_port >= 0, "Invalid outport to configure next output simple" + assert port_id >= 0, "Invalid port ID to configure next output simple" + assert next_id >=0, "Invalid next ID to configure next output simple" + + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure next output - simple") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Next ID {next_id}") rule_no = cache_rule(TABLE_NEXT_SIMPLE, action) @@ -506,14 +588,14 @@ def rules_set_up_next_output_simple( 'match-fields': [ { 'match-field': 'next_id', - 'match-value': str(egress_port) + 'match-value': str(next_id) } ], 'action-name': 'FabricIngress.next.output_simple', 'action-params': [ { 'action-param': 'port_num', - 'action-value': str(egress_port) + 'action-value': str(next_id) } ] } @@ -523,12 +605,17 @@ def rules_set_up_next_output_simple( return rules_next_output_simple def rules_set_up_next_hashed( - egress_port : int, + port_id : int, next_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert egress_port >= 0, "Invalid outport to configure next routing hashed" + assert port_id >= 0, "Invalid port ID to configure next routing hashed" assert next_id >=0, "Invalid next ID to configure next routing hashed" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure next output - hashed routing") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Next ID {next_id}") + rule_no = cache_rule(TABLE_NEXT_HASHED, action) rules_next_hashed = [] @@ -561,16 +648,23 @@ def rules_set_up_next_hashed( ################################### def rules_set_up_next_profile_hashed_routing( - egress_port : int, + port_id : int, next_id : int, eth_src : str, eth_dst : str, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert egress_port >= 0, "Invalid outport to configure next profile for hashed routing" + assert port_id >= 0, "Invalid port ID to configure next profile for hashed routing" assert next_id >=0, "Invalid next ID to configure next profile for hashed routing" assert chk_address_mac(eth_src), "Invalid source Ethernet address to configure next profile for hashed routing" assert chk_address_mac(eth_dst), "Invalid destination Ethernet address to configure next profile for hashed routing" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure next hashed profile for routing") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Next ID {next_id}") + LOGGER.info(f"================== Ethernet Src {eth_src}") + LOGGER.info(f"================== Ethernet Dst {eth_dst}") + rule_no = cache_rule(ACTION_PROFILE_NEXT_HASHED, action) rules_next_profile_hashed_rt = [] @@ -585,7 +679,7 @@ def rules_set_up_next_profile_hashed_routing( 'action-params': [ { 'action-param': 'port_num', - 'action-value': str(egress_port) + 'action-value': str(next_id) }, { 'action-param': 'smac', @@ -603,13 +697,21 @@ def rules_set_up_next_profile_hashed_routing( return rules_next_profile_hashed_rt def rules_set_up_routing( + port_id : int, ipv4_dst : str, ipv4_prefix_len : int, - egress_port : int, + next_id : int, action : ConfigActionEnum) -> List [Tuple]: # type: ignore + assert port_id >= 0, "Invalid port ID to configure routing" assert chk_address_ipv4(ipv4_dst), "Invalid destination IPv4 address to configure routing" assert chk_prefix_len_ipv4(ipv4_prefix_len), "Invalid IPv4 prefix length to configure routing" - assert egress_port >= 0, "Invalid outport to configure routing" + assert next_id >= 0, "Invalid next ID to configure routing" + + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure routing") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== IPv4 dst {ipv4_dst}/{ipv4_prefix_len}") + LOGGER.info(f"================== Next ID {next_id}") rule_no = cache_rule(TABLE_ROUTING_V4, action) @@ -630,7 +732,7 @@ def rules_set_up_routing( 'action-params': [ { 'action-param': 'next_id', - 'action-value': str(egress_port) + 'action-value': str(next_id) } ] } @@ -640,14 +742,23 @@ def rules_set_up_routing( return rules_routing def rules_set_up_next_routing_simple( - egress_port : int, + port_id : int, + next_id : 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 port_id >= 0, "Invalid port ID to configure next routing simple" + assert next_id >=0, "Invalid next ID 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" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure next routing - simple") + LOGGER.info(f"================== Port {port_id}") + LOGGER.info(f"================== Next ID {next_id}") + LOGGER.info(f"================== MAC src {eth_src}") + LOGGER.info(f"================== MAC dst {eth_dst}") + rule_no = cache_rule(TABLE_NEXT_SIMPLE, action) rules_next_routing_simple = [] @@ -660,14 +771,14 @@ def rules_set_up_next_routing_simple( 'match-fields': [ { 'match-field': 'next_id', - 'match-value': str(egress_port) + 'match-value': str(next_id) } ], 'action-name': 'FabricIngress.next.routing_simple', 'action-params': [ { 'action-param': 'port_num', - 'action-value': str(egress_port) + 'action-value': str(next_id) }, { 'action-param': 'smac', @@ -721,6 +832,12 @@ def rules_set_up_clone_session( assert egress_port >= 0, "Invalid egress port number to configure clone session" assert instance >= 0, "Invalid instance number to configure clone session" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure clone session") + LOGGER.info(f"================== Session ID {session_id}") + LOGGER.info(f"================== Egress port {egress_port}") + LOGGER.info(f"================== Instance {instance}") + rule_no = cache_rule(CLONE_SESSION, action) #TODO: For TNA pass also: packet_length_bytes = 128 @@ -756,21 +873,27 @@ def rules_set_up_clone_session( ################################### def rules_set_up_acl_filter_host( - ingress_port : int, + port_id : int, ip_address : str, prefix_len : int, ip_direction : str, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert ingress_port >= 0, "Invalid ingress port to configure ACL" + assert port_id >= 0, "Invalid port ID 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" ip_match = "ipv4_src" if ip_direction == "src" else "ipv4_dst" - prefix_len_hex = prefix_to_hex_mask(prefix_len) rule_no = cache_rule(TABLE_ACL, action) + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure host ACL rule") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== IP address {ip_address}") + LOGGER.info(f"================== IP address type {ip_match}") + LOGGER.info(f"================== IP prefix length {prefix_len}") + rules_acl = [] rules_acl.append( json_config_rule( @@ -781,7 +904,7 @@ def rules_set_up_acl_filter_host( 'match-fields': [ { 'match-field': 'ig_port', - 'match-value': str(ingress_port) + 'match-value': str(port_id) }, { 'match-field': ip_match, @@ -798,17 +921,23 @@ def rules_set_up_acl_filter_host( return rules_acl def rules_set_up_acl_filter_port( - ingress_port : int, + port_id : int, transport_port : int, transport_direction : str, action : ConfigActionEnum) -> List [Tuple]: # type: ignore - assert ingress_port >= 0, "Invalid ingress port to configure ACL" + assert port_id >= 0, "Invalid port ID to configure ACL" assert chk_transport_port(transport_port), "Invalid transport port to configure ACL" trn_match = "l4_sport" if transport_direction == "src" else "l4_dport" rule_no = cache_rule(TABLE_ACL, action) + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure port ACL rule") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Transport port {transport_port}") + LOGGER.info(f"================== Transport port type {transport_direction}") + rules_acl = [] rules_acl.append( json_config_rule( @@ -819,7 +948,7 @@ def rules_set_up_acl_filter_port( 'match-fields': [ { 'match-field': 'ig_port', - 'match-value': str(ingress_port) + 'match-value': str(port_id) }, { 'match-field': trn_match, diff --git a/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_config.py b/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_config.py index da5d6db07..dc7b6e6ba 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_config.py +++ b/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_config.py @@ -124,7 +124,7 @@ def rules_set_up_int_recirculation_ports( for port in recirculation_port_list: rules_list.extend( rules_set_up_port( - port=port, + port_id=port, port_type=port_type, fwd_type=fwd_type, vlan_id=vlan_id, diff --git a/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_service_handler.py index 5cc6e24c8..f3d38d593 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_int/p4_fabric_tna_int_service_handler.py @@ -410,6 +410,13 @@ class P4FabricINTServiceHandler(_ServiceHandler): dev_name = device_obj.name rules = [] + port_id = self.__switch_info[dev_name][PORT_INT][PORT_ID] + next_id = port_id + mac_src = self.__switch_info[dev_name][MAC] + collector_mac_dst = self.__int_collector_mac + collector_ip_dst = self.__int_collector_ip + vlan_id = self.__int_vlan_id + ### INT reporting rules try: rules += rules_set_up_int_watchlist(action=action) @@ -422,7 +429,7 @@ class P4FabricINTServiceHandler(_ServiceHandler): recirculation_port_list=self.__switch_info[dev_name][RECIRCULATION_PORT_LIST], port_type=PORT_TYPE_INT, fwd_type=FORWARDING_TYPE_UNICAST_IPV4, - vlan_id=self.__int_vlan_id, + vlan_id=vlan_id, action=action ) except Exception as ex: @@ -463,10 +470,10 @@ class P4FabricINTServiceHandler(_ServiceHandler): ### INT port setup rules try: rules += rules_set_up_port( - port=self.__switch_info[dev_name][PORT_INT][PORT_ID], + port_id=port_id, port_type=PORT_TYPE_HOST, fwd_type=FORWARDING_TYPE_BRIDGING, - vlan_id=self.__int_vlan_id, + vlan_id=vlan_id, action=action ) except Exception as ex: @@ -476,13 +483,17 @@ class P4FabricINTServiceHandler(_ServiceHandler): ### INT port forwarding rules try: rules += rules_set_up_fwd_bridging( - vlan_id=self.__int_vlan_id, - eth_dst=self.__int_collector_mac, - egress_port=self.__switch_info[dev_name][PORT_INT][PORT_ID], + port_id=port_id, + vlan_id=vlan_id, + eth_dst=collector_mac_dst, + next_id=next_id, action=action ) - rules += rules_set_up_next_output_simple( - egress_port=self.__switch_info[dev_name][PORT_INT][PORT_ID], + rules += rules_set_up_next_profile_hashed_routing( + port_id=port_id, + next_id=next_id, + eth_src=mac_src, + eth_dst=collector_mac_dst, action=action ) except Exception as ex: @@ -491,16 +502,16 @@ class P4FabricINTServiceHandler(_ServiceHandler): ### INT packet routing rules try: - rules += rules_set_up_next_routing_simple( - egress_port=self.__switch_info[dev_name][PORT_INT][PORT_ID], - eth_src=self.__switch_info[dev_name][MAC], - eth_dst=self.__int_collector_mac, + rules += rules_set_up_next_hashed( + port_id=port_id, + next_id=next_id, action=action ) rules += rules_set_up_routing( - ipv4_dst=self.__int_collector_ip, + port_id=port_id, + ipv4_dst=collector_ip_dst, ipv4_prefix_len=32, - egress_port=self.__switch_info[dev_name][PORT_INT][PORT_ID], + next_id=next_id, action=action ) except Exception as ex: @@ -587,13 +598,14 @@ class P4FabricINTServiceHandler(_ServiceHandler): return # Start the INT collector - c_id = None + c_uuid = None try: telemetry_frontend_client = TelemetryFrontendClient() c_id: CollectorId = telemetry_frontend_client.StartCollector(collect_int) # type: ignore - assert c_id.collector_id.uuid, "INT collector failed to start" + c_uuid = c_id.collector_id.uuid + assert c_uuid, "INT collector failed to start" except Exception as ex: LOGGER.error(f"INT collector cannot be initialized: Failed to start the collector {ex}") return - LOGGER.info(f"INT collector with ID {c_id.collector_id.uuid} is successfully invoked") + LOGGER.info(f"INT collector with ID {c_uuid} is successfully invoked") diff --git a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py b/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py deleted file mode 100644 index d2a13ef03..000000000 --- a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_config.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2022-2025 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 L2 forwarding based on the SD-Fabric dataplane model. -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 -""" - -from common.proto.context_pb2 import ConfigActionEnum - -from service.service.service_handlers.p4_fabric_tna_commons.p4_fabric_tna_commons import * - -def rules_set_up_port_host( - port : int, - vlan_id : int, - action : ConfigActionEnum, # type: ignore - fwd_type=FORWARDING_TYPE_BRIDGING, - eth_type=ETHER_TYPE_IPV4): - # This is a host facing port - port_type = PORT_TYPE_HOST - - return rules_set_up_port( - port=port, - port_type=port_type, - fwd_type=fwd_type, - vlan_id=vlan_id, - action=action, - eth_type=eth_type - ) - -def rules_set_up_port_switch( - port : int, - vlan_id : int, - action : ConfigActionEnum, # type: ignore - fwd_type=FORWARDING_TYPE_BRIDGING, - eth_type=ETHER_TYPE_IPV4): - # This is a switch facing port - port_type = PORT_TYPE_SWITCH - - return rules_set_up_port( - port=port, - port_type=port_type, - fwd_type=fwd_type, - vlan_id=vlan_id, - action=action, - eth_type=eth_type - ) diff --git a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_service_handler.py index aa8bd6209..1c1818e56 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_l2_simple/p4_fabric_tna_l2_simple_service_handler.py @@ -28,8 +28,6 @@ from service.service.service_handler_api.SettingsHandler import SettingsHandler from service.service.service_handlers.p4_fabric_tna_commons.p4_fabric_tna_commons import * from service.service.task_scheduler.TaskExecutor import TaskExecutor -from .p4_fabric_tna_l2_simple_config import * - LOGGER = logging.getLogger(__name__) METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'p4_fabric_tna_l2_simple'}) @@ -411,6 +409,9 @@ class P4FabricL2SimpleServiceHandler(_ServiceHandler): def _create_rules(self, device_obj : Device, port_id : int, action : ConfigActionEnum): # type: ignore dev_name = device_obj.name + # TODO: Fix + next_id = 2 if port_id == 1 else 1 + host_facing_port = self._is_host_facing_port(dev_name, port_id) LOGGER.info(f"\t | Service endpoint is host facing: {"True" if host_facing_port else "False"}") @@ -434,29 +435,46 @@ class P4FabricL2SimpleServiceHandler(_ServiceHandler): LOGGER.error("Error while creating port setup rules") raise Exception(ex) - fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) - for mac in fwd_list: - LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for host MAC: {mac}") + fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=next_id) + for mac_dst in fwd_list: + LOGGER.info(f"Switch {dev_name} - Port {port_id} - Creating rule for destination MAC: {mac_dst}") try: ### Bridging rules rules += rules_set_up_fwd_bridging( + port_id=port_id, vlan_id=self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id), - eth_dst=mac, - egress_port=port_id, + eth_dst=mac_dst, + next_id=next_id, action=action ) except Exception as ex: - LOGGER.error("Error while creating bridging rules") + LOGGER.error("Error while creating rule for bridging") raise Exception(ex) - try: - ### Next output rule - rules += rules_set_up_next_output_simple( - egress_port=port_id, - action=action - ) - except Exception as ex: - LOGGER.error("Error while creating next output L2 rules") - raise Exception(ex) + src_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) + for mac_src in src_list: + try: + ### Next profile for hashed routing + rules += rules_set_up_next_profile_hashed_routing( + port_id=port_id, + next_id=next_id, + eth_src=mac_src, + eth_dst=mac_dst, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating rule for next hashed profile") + raise Exception(ex) + + try: + ### Next hashed port + rules += rules_set_up_next_hashed( + port_id=port_id, + next_id=next_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating rule for next hashed port") + raise Exception(ex) return rules diff --git a/src/service/service/service_handlers/p4_fabric_tna_l3/p4_fabric_tna_l3_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_l3/p4_fabric_tna_l3_service_handler.py index 4f6927829..730c7c790 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_l3/p4_fabric_tna_l3_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_l3/p4_fabric_tna_l3_service_handler.py @@ -341,6 +341,8 @@ class P4FabricL3ServiceHandler(_ServiceHandler): assert port_id >= 0, f"Switch {switch_name} - Invalid P4 switch port ID" port_type = port[PORT_TYPE] assert port_type in PORT_TYPES_STR_VALID, f"Switch {switch_name} - Valid P4 switch port types are: {','.join(PORT_TYPES_STR_VALID)}" + vlan_id = port[VLAN_ID] + assert chk_vlan_id(vlan_id), f"Switch {switch_name} - Invalid VLAN ID for port {port_id}" if switch_name not in self.__port_map: self.__port_map[switch_name] = {} @@ -349,6 +351,7 @@ class P4FabricL3ServiceHandler(_ServiceHandler): self.__port_map[switch_name][port_key] = {} self.__port_map[switch_name][port_key][PORT_ID] = port_id self.__port_map[switch_name][port_key][PORT_TYPE] = port_type + self.__port_map[switch_name][port_key][VLAN_ID] = vlan_id self.__port_map[switch_name][port_key][ROUTING_LIST] = [] # Routing list @@ -401,29 +404,97 @@ class P4FabricL3ServiceHandler(_ServiceHandler): return switch_entry[port_key] + def _get_port_type_of_switch_port(self, switch_name : str, port_id : int) -> str: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[PORT_TYPE] + + def _get_vlan_id_of_switch_port(self, switch_name : str, port_id : int) -> int: + switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) + return switch_port_entry[VLAN_ID] + def _get_routing_list_of_switch_port(self, switch_name : str, port_id : int) -> List [Tuple]: switch_port_entry = self._get_switch_port_in_port_map(switch_name, port_id) return switch_port_entry[ROUTING_LIST] + def _is_host_facing_port(self, switch_name : str, port_id : int) -> bool: + return self._get_port_type_of_switch_port(switch_name, port_id) == PORT_TYPE_HOST + def _create_rules(self, device_obj : Device, port_id : int, action : ConfigActionEnum): # type: ignore dev_name = device_obj.name + # TODO: Fix + next_id = 2 if port_id ==1 else 1 + + host_facing_port = self._is_host_facing_port(dev_name, port_id) + LOGGER.info(f"\t | Service endpoint is host facing: {"True" if host_facing_port else "False"}") + rules = [] + try: + ### Port setup rules + if host_facing_port: + rules += rules_set_up_port_host( + port=port_id, + vlan_id=self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id), + action=action + ) + else: + rules += rules_set_up_port_switch( + port=port_id, + vlan_id=self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id), + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating port setup rules") + raise Exception(ex) + ### Static routing rules - routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=port_id) + routing_list = self._get_routing_list_of_switch_port(switch_name=dev_name, port_id=next_id) for rt_entry in routing_list: try: - rules += rules_set_up_next_routing_simple( - egress_port=port_id, + ### Bridging rules + rules += rules_set_up_fwd_bridging( + port_id=port_id, + vlan_id=self._get_vlan_id_of_switch_port(switch_name=dev_name, port_id=port_id), + eth_dst=rt_entry[MAC_DST], + next_id=next_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating rule for bridging") + raise Exception(ex) + + try: + ### Next profile for hashed routing + rules += rules_set_up_next_profile_hashed_routing( + port_id=port_id, + next_id=next_id, eth_src=rt_entry[MAC_SRC], eth_dst=rt_entry[MAC_DST], action=action ) + except Exception as ex: + LOGGER.error("Error while creating rule for next hashed profile") + raise Exception(ex) + + try: + ### Next hashed port + rules += rules_set_up_next_hashed( + port_id=port_id, + next_id=next_id, + action=action + ) + except Exception as ex: + LOGGER.error("Error while creating rule for next hashed port") + raise Exception(ex) + + try: + # Routing destination rules += rules_set_up_routing( + port_id=port_id, ipv4_dst=rt_entry[IPV4_DST], ipv4_prefix_len=rt_entry[IPV4_PREFIX_LEN], - egress_port=port_id, + next_id=next_id, action=action ) except Exception as ex: diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py index feda2e79c..faea1d3d6 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_config.py @@ -120,7 +120,8 @@ QOS_TYPE_TO_DESC_MAP = { } -def rules_set_up_upf_interfaces( +def rules_set_up_upf_interface( + port_id : int, ipv4_dst : str, ipv4_prefix_len : int, gtpu_value : int, @@ -138,6 +139,15 @@ def rules_set_up_upf_interfaces( else: # Packet does not carry a GTP header (DL packet) action_name = "FabricIngress.upf.iface_core" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure UPF interface") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== IPv4 dst {ipv4_dst}") + LOGGER.info(f"================== IPv4 prefix {ipv4_prefix_len}") + LOGGER.info(f"================== GTP-U value {gtpu_value}") + LOGGER.info(f"================== Slice ID {slice_id}") + LOGGER.info(f"================== Action {action_name}") + rule_no = cache_rule(TABLE_UPF_INTERFACES, action) rules_upf_interfaces = [] @@ -176,6 +186,7 @@ def rules_set_up_upf_interfaces( ################################### def rules_set_up_upf_uplink_sessions( + port_id : int, tun_ip_address : str, teid : int, session_meter_id : int, @@ -184,6 +195,13 @@ def rules_set_up_upf_uplink_sessions( assert teid >= 0, "Invalid tunnel endpoint identifier (TEID) to configure UPF uplink session" assert session_meter_id >= 0, "Invalid session meter identifier to configure UPF uplink session" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure UL session") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Tunnel IP {tun_ip_address}") + LOGGER.info(f"================== TEID {teid}") + LOGGER.info(f"================== Session meter ID {session_meter_id}") + rule_no = cache_rule(TABLE_UPF_UL_SESSIONS, action) rules_upf_ul_session = [] @@ -218,6 +236,7 @@ def rules_set_up_upf_uplink_sessions( return rules_upf_ul_session def rules_set_up_upf_uplink_terminations( + port_id : int, ue_session_id : str, app_id : int, ctr_id : int, @@ -230,6 +249,15 @@ def rules_set_up_upf_uplink_terminations( assert app_meter_id >= 0, "Invalid app meter identifier to configure UPF uplink termination" assert tc_id >= 0, "Invalid tc identifier to configure UPF uplink termination" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure UL termination") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== UE session ID {ue_session_id}") + LOGGER.info(f"================== App ID {app_id}") + LOGGER.info(f"================== Ctr ID {ctr_id}") + LOGGER.info(f"================== App meter ID {app_meter_id}") + LOGGER.info(f"================== TC ID {tc_id}") + rule_no = cache_rule(TABLE_UPF_UL_TERM, action) rules_upf_ul_termination = [] @@ -280,6 +308,7 @@ def rules_set_up_upf_uplink_terminations( ################################### def rules_set_up_upf_downlink_sessions( + port_id : int, ipv4_dst : str, session_meter_id : int, tun_peer_id : int, @@ -288,6 +317,13 @@ def rules_set_up_upf_downlink_sessions( assert session_meter_id >= 0, "Invalid session meter identifier to configure UPF downlink session" assert tun_peer_id >= 0, "Invalid tunnel peer identifier to configure UPF downlink session" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure DL session") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== IP dst {ipv4_dst}") + LOGGER.info(f"================== Session meter ID {session_meter_id}") + LOGGER.info(f"================== Tunnel peer ID {tun_peer_id}") + rule_no = cache_rule(TABLE_UPF_DL_SESSIONS, action) rules_upf_dl_session = [] @@ -322,6 +358,7 @@ def rules_set_up_upf_downlink_sessions( return rules_upf_dl_session def rules_set_up_upf_downlink_terminations( + port_id : int, ue_session_id : str, app_id : int, ctr_id : int, @@ -338,6 +375,17 @@ def rules_set_up_upf_downlink_terminations( assert teid >= 0, "Invalid tunnel endpoint identifier (TEID) to configure UPF downlink termination" assert qfi >= 0, "Invalid QoS flow identifier (QFI) to configure UPF downlink termination" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure DL termination") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== UE session ID {ue_session_id}") + LOGGER.info(f"================== App ID {app_id}") + LOGGER.info(f"================== Ctr ID {ctr_id}") + LOGGER.info(f"================== App meter ID {app_meter_id}") + LOGGER.info(f"================== TC ID {tc_id}") + LOGGER.info(f"================== TEID {teid}") + LOGGER.info(f"================== QFI {qfi}") + rule_no = cache_rule(TABLE_UPF_DL_TERM, action) rules_upf_dl_termination = [] @@ -388,12 +436,19 @@ def rules_set_up_upf_downlink_terminations( return rules_upf_dl_termination def rules_set_up_upf_downlink_ig_tunnel_peers( + port_id : int, tun_peer_id : int, tun_dst_addr : str, action : ConfigActionEnum) -> List [Tuple]: # type: ignore assert tun_peer_id >= 0, "Invalid tunnel peer identifier to configure UPF downlink ingress tunnel peers" assert chk_address_ipv4(tun_dst_addr), "Invalid tunnel destination IPv4 address to configure UPF downlink ingress tunnel peers" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure IG tunnel peer") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Tunnel peer ID {tun_peer_id}") + LOGGER.info(f"================== Tunnel IP dst {tun_dst_addr}") + rule_no = cache_rule(TABLE_UPF_DL_IG_TUN_PEERS, action) rules_upf_dl_ig_tun_peers = [] @@ -424,6 +479,7 @@ def rules_set_up_upf_downlink_ig_tunnel_peers( return rules_upf_dl_ig_tun_peers def rules_set_up_upf_downlink_eg_tunnel_peers( + port_id : int, tun_peer_id : int, tun_src_addr : str, tun_dst_addr : str, @@ -434,6 +490,14 @@ def rules_set_up_upf_downlink_eg_tunnel_peers( assert chk_address_ipv4(tun_dst_addr), "Invalid tunnel destination IPv4 address to configure UPF downlink egress tunnel peers" assert chk_transport_port(tun_src_port), "Invalid tunnel source transport port to configure UPF downlink egress tunnel peers" + LOGGER.info("==================================================================================") + LOGGER.info("================== About to configure EG tunnel peer") + LOGGER.info(f"================== Port ID {port_id}") + LOGGER.info(f"================== Tunnel peer ID {tun_peer_id}") + LOGGER.info(f"================== Tunnel IP src {tun_src_addr}") + LOGGER.info(f"================== Tunnel IP dst {tun_dst_addr}") + LOGGER.info(f"================== Tunnel Port src {tun_src_port}") + rule_no = cache_rule(TABLE_UPF_DL_EG_TUN_PEERS, action) rules_upf_dl_eg_tun_peers = [] diff --git a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py index c378a0738..f9d4ca20f 100644 --- a/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py +++ b/src/service/service/service_handlers/p4_fabric_tna_upf/p4_fabric_tna_upf_service_handler.py @@ -661,7 +661,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): # Port setup rules try: rules += rules_set_up_port( - port=port_id, + port_id=port_id, port_type=PORT_TYPE_HOST, fwd_type=FORWARDING_TYPE_BRIDGING, vlan_id=vlan_id, @@ -673,7 +673,8 @@ class P4FabricUPFServiceHandler(_ServiceHandler): ### UPF rules try: - rules += rules_set_up_upf_interfaces( + rules += rules_set_up_upf_interface( + port_id=port_id, ipv4_dst=self.__upf[UPLINK_IP], # UPF's N3 interface ipv4_prefix_len=32, gtpu_value=GTPU_VALID, @@ -686,6 +687,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: rules += rules_set_up_upf_uplink_sessions( + port_id=port_id, tun_ip_address=self.__upf[UPLINK_IP], # UPF's N3 interface teid=self.__upf[TEID], session_meter_id=DEF_SESSION_METER_ID, @@ -699,6 +701,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): for _, ue_info in self.__ue_map.items(): try: rules += rules_set_up_upf_uplink_terminations( + port_id=port_id, ue_session_id=ue_info[UE_IP], # UE's IPv4 address app_id=self.__upf[APP_ID], ctr_id=self.__upf[CTR_ID], @@ -711,7 +714,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): raise Exception(ex) # L2 Forwarding rules - fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) + fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=next_id) for host_map in fwd_list: mac_dst = host_map[HOST_MAC] label = host_map[HOST_LABEL] @@ -719,9 +722,10 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Bridging rules rules += rules_set_up_fwd_bridging( + port_id=port_id, vlan_id=vlan_id, eth_dst=mac_dst, - egress_port=port_id, + next_id=next_id, action=action ) except Exception as ex: @@ -731,6 +735,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Pre-next VLAN rule rules += rules_set_up_pre_next_vlan( + port_id=port_id, next_id=next_id, vlan_id=vlan_id, action=action @@ -747,7 +752,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Next profile for hashed routing rules += rules_set_up_next_profile_hashed_routing( - egress_port=port_id, + port_id=port_id, next_id=next_id, eth_src=rt_entry[MAC_SRC], # UPF's N6 interface (self.__upf[DOWNLINK_MAC]) eth_dst=rt_entry[MAC_DST], # Data network's N6 interface @@ -760,15 +765,16 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Next hashed for routing rules += rules_set_up_next_hashed( - egress_port=port_id, + port_id=port_id, next_id=next_id, action=action ) ### Route towards destination rules += rules_set_up_routing( + port_id=port_id, ipv4_dst=rt_entry[IPV4_DST], ipv4_prefix_len=rt_entry[IPV4_PREFIX_LEN], - egress_port=port_id, + next_id=next_id, action=action ) except Exception as ex: @@ -791,7 +797,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): # Port setup try: rules += rules_set_up_port( - port=port_id, + port_id=port_id, port_type=PORT_TYPE_HOST, fwd_type=FORWARDING_TYPE_BRIDGING, vlan_id=vlan_id, @@ -804,7 +810,8 @@ class P4FabricUPFServiceHandler(_ServiceHandler): ### UPF for _, ue_info in self.__ue_map.items(): try: - rules += rules_set_up_upf_interfaces( + rules += rules_set_up_upf_interface( + port_id=port_id, ipv4_dst=ue_info[UE_IP], ipv4_prefix_len=32, gtpu_value=GTPU_INVALID, @@ -817,6 +824,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: rules += rules_set_up_upf_downlink_sessions( + port_id=port_id, ipv4_dst=ue_info[UE_IP], session_meter_id=ue_info[PDU_LIST][0][PDU_SESSION_ID], # Should match DEF_SESSION_METER_ID tun_peer_id=self.__upf[TUNNEL_PEER_ID], @@ -828,6 +836,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: rules += rules_set_up_upf_downlink_terminations( + port_id=port_id, ue_session_id=ue_info[UE_IP], # UE's IPv4 address app_id=self.__upf[APP_ID], ctr_id=self.__upf[CTR_ID], @@ -843,6 +852,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: rules += rules_set_up_upf_downlink_ig_tunnel_peers( + port_id=port_id, tun_peer_id=self.__upf[TUNNEL_PEER_ID], tun_dst_addr=self.__upf[UPLINK_IP], # UPF's N3 interface action=action @@ -853,6 +863,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: rules += rules_set_up_upf_downlink_eg_tunnel_peers( + port_id=port_id, tun_peer_id=self.__upf[TUNNEL_PEER_ID], tun_src_addr=self.__upf[UPLINK_IP], # UPF's N3 interface tun_dst_addr=self.__gnb[IP], # gNB's N3 interface @@ -864,7 +875,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): raise Exception(ex) # L2 Forwarding - fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=port_id) + fwd_list = self._get_fwd_list_of_switch_port(switch_name=dev_name, port_id=next_id) for host_map in fwd_list: mac_dst = host_map[HOST_MAC] label = host_map[HOST_LABEL] @@ -872,9 +883,10 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Bridging rules += rules_set_up_fwd_bridging( + port_id=port_id, vlan_id=vlan_id, eth_dst=mac_dst, - egress_port=port_id, + next_id=next_id, action=action ) except Exception as ex: @@ -884,6 +896,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Pre-next VLAN rules += rules_set_up_pre_next_vlan( + port_id=port_id, next_id=next_id, vlan_id=vlan_id, action=action @@ -900,7 +913,7 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Next profile for hashed routing rules += rules_set_up_next_profile_hashed_routing( - egress_port=port_id, + port_id=port_id, next_id=next_id, eth_src=rt_entry[MAC_SRC], # UPF's N3 interface (self.__upf[UPLINK_MAC]) eth_dst=rt_entry[MAC_DST], # gNB's N3 interface (self.__gnb[MAC]) @@ -913,15 +926,16 @@ class P4FabricUPFServiceHandler(_ServiceHandler): try: ### Next hashed for routing rules += rules_set_up_next_hashed( - egress_port=port_id, + port_id=port_id, next_id=next_id, action=action ) ### Route towards destination rules += rules_set_up_routing( + port_id=port_id, ipv4_dst=rt_entry[IPV4_DST], ipv4_prefix_len=rt_entry[IPV4_PREFIX_LEN], - egress_port=port_id, + next_id=next_id, action=action ) except Exception as ex: diff --git a/src/telemetry/backend/service/collectors/int_collector/INTCollector.py b/src/telemetry/backend/service/collectors/int_collector/INTCollector.py index 0f35882f0..84352f417 100644 --- a/src/telemetry/backend/service/collectors/int_collector/INTCollector.py +++ b/src/telemetry/backend/service/collectors/int_collector/INTCollector.py @@ -23,8 +23,6 @@ from datetime import datetime from telemetry.backend.service.collector_api._Collector import _Collector from scapy.all import * -import struct -import socket import ipaddress from .INTCollectorCommon import IntDropReport, IntLocalReport, IntFixedReport, FlowInfo, IPPacket, UDPPacket -- GitLab