diff --git a/src/service/service/service_handlers/l3slice_ietfslice/ConfigRules.py b/src/service/service/service_handlers/l3slice_ietfslice/ConfigRules.py index 466c53fe4b576f794091112f9c5958f2e17d5fca..173d4ba10dbf1f6a8aead912a2a1435632f94569 100644 --- a/src/service/service/service_handlers/l3slice_ietfslice/ConfigRules.py +++ b/src/service/service/service_handlers/l3slice_ietfslice/ConfigRules.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# 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. @@ -22,8 +22,114 @@ from common.tools.object_factory.ConfigRule import ( from context.client.ContextClient import ContextClient +def build_match_criterion( + vlan: str, + src_ip: str, + src_port: str, + dst_ip: str, + dst_port: str, + target_conn_group_id: str = "line1", + index: int = 1, +) -> Dict: + """ + Build the match-criterion structure used in the 'service-match-criteria'. + """ + return { + "index": index, + "match-type": [ + {"type": "ietf-network-slice-service:vlan", "value": [vlan]}, + { + "type": "ietf-network-slice-service:source-ip-prefix", + "value": [src_ip], + }, + { + "type": "ietf-network-slice-service:source-tcp-port", + "value": [src_port], + }, + { + "type": "ietf-network-slice-service:destination-ip-prefix", + "value": [dst_ip], + }, + { + "type": "ietf-network-slice-service:destination-tcp-port", + "value": [dst_port], + }, + ], + "target-connection-group-id": target_conn_group_id, + } + + +def build_sdp( + sdp_id: str, + node_id: str, + mgmt_ip: str, + ac_node_id: str, + ac_ep_id: str, + match_criterion: Dict, + attachment_id: str = "0", + attachment_description: str = "dsc", +) -> Dict: + """ + Build the sdp structure used in the 'slice_service' dictionary. + """ + return { + "id": sdp_id, + "node-id": node_id, + "sdp-ip-address": [mgmt_ip], + "service-match-criteria": {"match-criterion": [match_criterion]}, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": attachment_id, + "description": attachment_description, + "ac-node-id": ac_node_id, + "ac-tp-id": ac_ep_id, + } + ] + }, + } + + +def build_slo_policy_bound( + one_way_delay: int, one_way_bandwidth: int, one_way_packet_loss: float +) -> List[Dict]: + """ + Build the 'metric-bound' portion of the 'slo-policy' dictionary. + """ + return [ + { + "metric-type": "ietf-network-slice-service:one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": one_way_delay, + }, + { + "metric-type": "ietf-network-slice-service:one-way-bandwidth", + "metric-unit": "Mbps", + "bound": one_way_bandwidth, + }, + { + "metric-type": "ietf-network-slice-service:two-way-packet-loss", + "metric-unit": "percentage", + "percentile-value": one_way_packet_loss, + }, + ] + + +def _get_device_endpoint_name(device_obj, endpoint_uuid: str) -> str: + """ + Given a device object and an endpoint UUID, return the device endpoint name. + Raises an exception if not found. + """ + for d_ep in device_obj.device_endpoints: + if d_ep.endpoint_id.endpoint_uuid.uuid == endpoint_uuid: + return d_ep.name + raise Exception("Endpoint not found") + + def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: operation_type: str = json_settings["operation_type"] + + # Source parameters src_node_id: str = json_settings["src_node_id"] src_mgmt_ip_address: str = json_settings["src_mgmt_ip_address"] src_ac_node_id: str = json_settings["src_ac_node_id"] @@ -38,6 +144,8 @@ def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: source_one_way_packet_loss: float = float( json_settings["source_one_way_packet_loss"] ) + + # Destination parameters dst_node_id: str = json_settings["dst_node_id"] dst_mgmt_ip_address: str = json_settings["dst_mgmt_ip_address"] dst_ac_node_id: str = json_settings["dst_ac_node_id"] @@ -54,101 +162,47 @@ def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: destination_one_way_packet_loss: float = float( json_settings["destination_one_way_packet_loss"] ) + + # Slice ID slice_id: str = json_settings["slice_id"] - sdps = [ - { - "id": "1", - "node-id": src_node_id, - "sdp-ip-address": [src_mgmt_ip_address], - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": [ - { - "type": "ietf-network-slice-service:vlan", - "value": [src_vlan], - }, - { - "type": "ietf-network-slice-service:source-ip-prefix", - "value": [src_source_ip_prefix], - }, - { - "type": "ietf-network-slice-service:source-tcp-port", - "value": [src_source_tcp_port], - }, - { - "type": "ietf-network-slice-service:destination-ip-prefix", - "value": [src_destination_ip_prefix], - }, - { - "type": "ietf-network-slice-service:destination-tcp-port", - "value": [src_destination_tcp_port], - }, - ], - "target-connection-group-id": "line1", - } - ] - }, - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "0", - "description": "dsc", - "ac-node-id": src_ac_node_id, - "ac-tp-id": src_ac_ep_id, - } - ] - }, - }, - { - "id": "2", - "node-id": dst_node_id, - "sdp-ip-address": [dst_mgmt_ip_address], - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": [ - { - "type": "ietf-network-slice-service:vlan", - "value": [dst_vlan], - }, - { - "type": "ietf-network-slice-service:source-ip-prefix", - "value": [dst_source_ip_prefix], - }, - { - "type": "ietf-network-slice-service:source-tcp-port", - "value": [dst_source_tcp_port], - }, - { - "type": "ietf-network-slice-service:destination-ip-prefix", - "value": [dst_destination_ip_prefix], - }, - { - "type": "ietf-network-slice-service:destination-tcp-port", - "value": [dst_destination_tcp_port], - }, - ], - "target-connection-group-id": "line1", - } - ] - }, - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "0", - "description": "dsc", - "ac-node-id": dst_ac_node_id, - "ac-tp-id": dst_ac_ep_id, - } - ] - }, - }, - ] + # build source & destination match criteria + src_match_criterion = build_match_criterion( + vlan=src_vlan, + src_ip=src_source_ip_prefix, + src_port=src_source_tcp_port, + dst_ip=src_destination_ip_prefix, + dst_port=src_destination_tcp_port, + ) + dst_match_criterion = build_match_criterion( + vlan=dst_vlan, + src_ip=dst_source_ip_prefix, + src_port=dst_source_tcp_port, + dst_ip=dst_destination_ip_prefix, + dst_port=dst_destination_tcp_port, + ) + + # Build SDPs + sdp_src = build_sdp( + sdp_id="1", + node_id=src_node_id, + mgmt_ip=src_mgmt_ip_address, + ac_node_id=src_ac_node_id, + ac_ep_id=src_ac_ep_id, + match_criterion=src_match_criterion, + ) + sdp_dst = build_sdp( + sdp_id="2", + node_id=dst_node_id, + mgmt_ip=dst_mgmt_ip_address, + ac_node_id=dst_ac_node_id, + ac_ep_id=dst_ac_ep_id, + match_criterion=dst_match_criterion, + ) + sdps = [sdp_src, sdp_dst] + + # Build connection-groups connection_groups = [ { "id": "line1", @@ -160,23 +214,11 @@ def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: "p2p-receiver-sdp": "2", "service-slo-sle-policy": { "slo-policy": { - "metric-bound": [ - { - "metric-type": "ietf-network-slice-service:one-way-delay-maximum", - "metric-unit": "milliseconds", - "bound": source_one_way_delay, - }, - { - "metric-type": "ietf-network-slice-service:one-way-bandwidth", - "metric-unit": "Mbps", - "bound": source_one_way_bandwidth, - }, - { - "metric-type": "ietf-network-slice-service:two-way-packet-loss", - "metric-unit": "percentage", - "percentile-value": source_one_way_packet_loss, - }, - ] + "metric-bound": build_slo_policy_bound( + one_way_delay=source_one_way_delay, + one_way_bandwidth=source_one_way_bandwidth, + one_way_packet_loss=source_one_way_packet_loss, + ) } }, }, @@ -186,29 +228,18 @@ def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: "p2p-receiver-sdp": "1", "service-slo-sle-policy": { "slo-policy": { - "metric-bound": [ - { - "metric-type": "ietf-network-slice-service:one-way-delay-maximum", - "metric-unit": "milliseconds", - "bound": destination_one_way_delay, - }, - { - "metric-type": "ietf-network-slice-service:one-way-bandwidth", - "metric-unit": "Mbps", - "bound": destination_one_way_bandwidth, - }, - { - "metric-type": "ietf-network-slice-service:two-way-packet-loss", - "metric-unit": "percentage", - "percentile-value": destination_one_way_packet_loss, - }, - ] + "metric-bound": build_slo_policy_bound( + one_way_delay=destination_one_way_delay, + one_way_bandwidth=destination_one_way_bandwidth, + one_way_packet_loss=destination_one_way_packet_loss, + ) } }, }, ], } ] + slice_service = { "id": slice_id, "description": "dsc", @@ -216,6 +247,7 @@ def setup_config_rules(service_uuid: str, json_settings: Dict) -> List[Dict]: "connection-groups": {"connection-group": connection_groups}, } slice_data_model = {"network-slice-services": {"slice-service": [slice_service]}} + json_config_rules = [ json_config_rule_set( "/service[{:s}]/IETFSlice".format(service_uuid), @@ -250,23 +282,15 @@ def get_link_ep_device_names( ep_device_id_1 = ep_ids[0].device_id ep_uuid_1 = ep_ids[0].endpoint_uuid.uuid device_obj_1 = context_client.GetDevice(ep_device_id_1) - for d_ep in device_obj_1.device_endpoints: - if d_ep.endpoint_id.endpoint_uuid.uuid == ep_uuid_1: - ep_name_1 = d_ep.name - break - else: - raise Exception("endpoint not found") + ep_name_1 = _get_device_endpoint_name(device_obj_1, ep_uuid_1) device_obj_name_1 = device_obj_1.name + ep_device_id_2 = ep_ids[1].device_id ep_uuid_2 = ep_ids[1].endpoint_uuid.uuid device_obj_2 = context_client.GetDevice(ep_device_id_2) - for d_ep in device_obj_2.device_endpoints: - if d_ep.endpoint_id.endpoint_uuid.uuid == ep_uuid_2: - ep_name_2 = d_ep.name - break - else: - raise Exception("endpoint not found") + ep_name_2 = _get_device_endpoint_name(device_obj_2, ep_uuid_2) device_obj_name_2 = device_obj_2.name + return ( device_obj_name_1, ep_name_1, diff --git a/src/service/service/service_handlers/l3slice_ietfslice/L3SliceIETFSliceServiceHandler.py b/src/service/service/service_handlers/l3slice_ietfslice/L3SliceIETFSliceServiceHandler.py index ea1c0f425f62d4f250c4e195ef91eb2a1e78e94d..0df8b56e3495dcf70dcfd78b8e3ea83bef93dc46 100644 --- a/src/service/service/service_handlers/l3slice_ietfslice/L3SliceIETFSliceServiceHandler.py +++ b/src/service/service/service_handlers/l3slice_ietfslice/L3SliceIETFSliceServiceHandler.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# 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. @@ -38,6 +38,30 @@ from .ConfigRules import ( teardown_config_rules, ) +RUNNING_RESOURCE_KEY = "running_ietf_slice" +CANDIDATE_RESOURCE_KEY = "candidate_ietf_slice" + +SDP_DIFF_RE = re.compile( + r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'sdps\'\]\[\'sdp\'\]\[(\d)\]$" +) +CONNECTION_GROUP_DIFF_RE = re.compile( + r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'connection-groups\'\]\[\'connection-group\'\]\[(\d)\]$" +) +MATCH_CRITERION_DIFF_RE = re.compile( + r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'sdps\'\]\[\'sdp\'\]\[(\d)\]\[\'service-match-criteria\'\]\[\'match-criterion\'\]\[(\d)\]$" +) + +RE_GET_ENDPOINT_FROM_INTERFACE = re.compile(r"^\/interface\[([^\]]+)\].*") + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool( + "Service", "Handler", labels={"handler": "l3slice_ietfslice"} +) + + +RAISE_IF_DIFFERS = True + class Ipv4Info(TypedDict): src_lan: str @@ -63,34 +87,68 @@ class ConnectivityConstructInfo: packet_loss: float = 0.0 -RUNNING_RESOURCE_KEY = "running_ietf_slice" -CANDIDATE_RESOURCE_KEY = "candidate_ietf_slice" - -SDP_DIFF_RE = re.compile( - r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'sdps\'\]\[\'sdp\'\]\[(\d)\]$" -) -CONNECTION_GROUP_DIFF_RE = re.compile( - r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'connection-groups\'\]\[\'connection-group\'\]\[(\d)\]$" -) -MATCH_CRITERION_DIFF_RE = re.compile( - r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'sdps\'\]\[\'sdp\'\]\[(\d)\]\[\'service-match-criteria\'\]\[\'match-criterion\'\]\[(\d)\]$" -) - -RE_GET_ENDPOINT_FROM_INTERFACE = re.compile(r"^\/interface\[([^\]]+)\].*") - -LOGGER = logging.getLogger(__name__) - -METRICS_POOL = MetricsPool( - "Service", "Handler", labels={"handler": "l3slice_ietfslice"} -) +def get_custom_config_rule( + service_config: ServiceConfig, resource_key: str +) -> Optional[ConfigRule]: + """ + Returns the ConfigRule from service_config matching the provided resource_key + if found, otherwise returns None. + """ + for cr in service_config.config_rules: + if ( + cr.WhichOneof("config_rule") == "custom" + and cr.custom.resource_key == resource_key + ): + return cr + return None -RAISE_IF_DIFFERS = True +def get_running_candidate_ietf_slice_data_diff(service_config: ServiceConfig) -> Dict: + """ + Loads the JSON from the running/candidate resource ConfigRules and returns + their DeepDiff comparison. + """ + running_cr = get_custom_config_rule(service_config, RUNNING_RESOURCE_KEY) + candidate_cr = get_custom_config_rule(service_config, CANDIDATE_RESOURCE_KEY) + running_value_dict = json.loads(running_cr.custom.resource_value) + candidate_value_dict = json.loads(candidate_cr.custom.resource_value) + return DeepDiff(running_value_dict, candidate_value_dict) + + +def extract_match_criterion_ipv4_info(match_criterion: Dict) -> Ipv4Info: + """ + Extracts IPv4 info from the match criterion dictionary. + """ + src_lan = dst_lan = src_port = dst_port = vlan = "" + for type_value in match_criterion["match-type"]: + m_type = type_value["type"] + val = type_value["value"][0] + if m_type == "ietf-network-slice-service:source-ip-prefix": + src_lan = val + elif m_type == "ietf-network-slice-service:destination-ip-prefix": + dst_lan = val + elif m_type == "ietf-network-slice-service:source-tcp-port": + src_port = val + elif m_type == "ietf-network-slice-service:destination-tcp-port": + dst_port = val + elif m_type == "ietf-network-slice-service:vlan": + vlan = val + return Ipv4Info( + src_lan=src_lan, + dst_lan=dst_lan, + src_port=src_port, + dst_port=dst_port, + vlan=vlan, + ) def get_removed_items( candidate_ietf_slice_dict: dict, running_ietf_slice_dict: dict ) -> dict: + """ + For the 'iterable_item_removed' scenario, returns dict with removed sdp / connection_group / match_criterion info. + Raises an exception if there's inconsistent data or multiple items removed (which is not supported). + """ removed_items = { "sdp": {"sdp_idx": None, "value": {}}, "connection_group": {"connection_group_idx": None, "value": {}}, @@ -100,20 +158,24 @@ def get_removed_items( "value": {}, }, } + running_slice_services = running_ietf_slice_dict["network-slice-services"][ "slice-service" ][0] - running_slice_sdps = [sdp["id"] for sdp in running_slice_services["sdps"]["sdp"]] candidate_slice_services = candidate_ietf_slice_dict["network-slice-services"][ "slice-service" ][0] + + running_slice_sdps = [sdp["id"] for sdp in running_slice_services["sdps"]["sdp"]] candidiate_slice_sdps = [ sdp["id"] for sdp in candidate_slice_services["sdps"]["sdp"] ] removed_sdps = set(running_slice_sdps) - set(candidiate_slice_sdps) + if len(removed_sdps) > 1: - raise Exception("Multiple SDPs removed") - removed_sdp_id = list(removed_sdps)[0] + raise Exception("Multiple SDPs removed - not supported.") + removed_sdp_id = removed_sdps.pop() + removed_items["sdp"]["sdp_idx"] = running_slice_sdps.index(removed_sdp_id) removed_items["sdp"]["value"] = next( sdp @@ -125,7 +187,7 @@ def get_removed_items( "match-criterion" ] if len(match_criteria) > 1: - raise Exception("Multiple match criteria found") + raise Exception("Multiple match criteria found - not supported") match_criterion = match_criteria[0] connection_grp_id = match_criterion["target-connection-group-id"] connection_groups = running_slice_services["connection-groups"]["connection-group"] @@ -136,6 +198,7 @@ def get_removed_items( ) removed_items["connection_group"]["connection_group_idx"] = connection_group[0] removed_items["connection_group"]["value"] = connection_group[1] + for sdp in running_slice_services["sdps"]["sdp"]: if sdp["id"] == removed_sdp_id: continue @@ -159,18 +222,13 @@ def get_removed_items( return removed_items -def extract_source_destination_device_endpoint_info( - device_ep_pairs: list, connection_group: Dict, candidate_connection_groups: List -) -> Tuple[DeviceEpInfo, DeviceEpInfo]: - connectivity_construct = connection_group["connectivity-construct"][0] - sender_sdp = connectivity_construct["p2p-sender-sdp"] - receiver_sdp = connectivity_construct["p2p-receiver-sdp"] - if sender_sdp == device_ep_pairs[0][4]: - ... - elif sender_sdp == device_ep_pairs[1][4]: - device_ep_pairs = device_ep_pairs[::-1] - else: - raise Exception("Sender SDP not found in device_ep_pairs") +def gather_connectivity_construct_info( + candidate_connection_groups: List[Dict], +) -> Dict[Tuple[str, str], ConnectivityConstructInfo]: + """ + Creates a dict mapping (sender_sdp, receiver_sdp) -> ConnectivityConstructInfo + from the given list of candidate connection groups. + """ cc_info: Dict[Tuple[str, str], ConnectivityConstructInfo] = {} for cg in candidate_connection_groups: for cc in cg["connectivity-construct"]: @@ -201,12 +259,39 @@ def extract_source_destination_device_endpoint_info( and metric_bound["metric-unit"] == "Mbps" ): cc_info[cc_key].bandwidth = int(metric_bound["bound"]) + return cc_info + + +def extract_source_destination_device_endpoint_info( + device_ep_pairs: list, connection_group: Dict, candidate_connection_groups: List +) -> Tuple[DeviceEpInfo, DeviceEpInfo]: + """ + Given device_ep_pairs, the relevant connection_group data, and all candidate + connection groups, figure out the final DeviceEpInfo for source and destination. + This includes computing the combined bandwidth, min delay, etc. + """ + connectivity_construct = connection_group["connectivity-construct"][0] + sender_sdp = connectivity_construct["p2p-sender-sdp"] + receiver_sdp = connectivity_construct["p2p-receiver-sdp"] + + # If the first pair is not the sender, we invert them + if sender_sdp == device_ep_pairs[0][4]: + ... + elif sender_sdp == device_ep_pairs[1][4]: + device_ep_pairs = device_ep_pairs[::-1] + else: + raise Exception("Sender SDP not found in device_ep_pairs") + + # Gather info from candidate connection groups + cc_info = gather_connectivity_construct_info(candidate_connection_groups) + source_delay = int(1e6) source_bandwidth = 0 source_packet_loss = 1.0 destination_delay = int(1e6) destination_bandwidth = 0 destination_packet_loss = 1.0 + if cc_info: common_sdps = set.intersection(*[set(key) for key in cc_info.keys()]) if len(cc_info) > 2 and len(common_sdps) != 1: @@ -216,9 +301,11 @@ def extract_source_destination_device_endpoint_info( if len(common_sdps) == 1: common_sdp = common_sdps.pop() elif len(common_sdps) == 2: + # Fallback if intersection is 2 => pick sender_sdp common_sdp = sender_sdp else: - raise Exception('Invalid number of common sdps') + raise Exception("Invalid number of common sdps") + for (sender, receiver), metrics in cc_info.items(): cc_bandwidth = metrics.bandwidth cc_max_delay = metrics.delay @@ -235,6 +322,7 @@ def extract_source_destination_device_endpoint_info( destination_delay = cc_max_delay if cc_packet_loss < destination_packet_loss: destination_packet_loss = cc_packet_loss + source_device_ep_info = DeviceEpInfo( ipv4_info=device_ep_pairs[0][5], node_name=device_ep_pairs[0][2], @@ -251,58 +339,45 @@ def extract_source_destination_device_endpoint_info( one_way_bandwidth=destination_bandwidth, one_way_packet_loss=destination_packet_loss, ) - return source_device_ep_info, destination_device_ep_info - - -def extract_match_criterion_ipv4_info( - match_criterion: Dict, -) -> Ipv4Info: - for type_value in match_criterion["match-type"]: - if type_value["type"] == "ietf-network-slice-service:source-ip-prefix": - src_lan = type_value["value"][0] - elif type_value["type"] == "ietf-network-slice-service:destination-ip-prefix": - dst_lan = type_value["value"][0] - elif type_value["type"] == "ietf-network-slice-service:source-tcp-port": - src_port = type_value["value"][0] - elif type_value["type"] == "ietf-network-slice-service:destination-tcp-port": - dst_port = type_value["value"][0] - elif type_value["type"] == "ietf-network-slice-service:vlan": - vlan = type_value["value"][0] - return Ipv4Info( - src_lan=src_lan, - dst_lan=dst_lan, - src_port=src_port, - dst_port=dst_port, - vlan=vlan, - ) - -def get_custom_config_rule( - service_config: ServiceConfig, resource_key: str -) -> Optional[ConfigRule]: - for cr in service_config.config_rules: - if ( - cr.WhichOneof("config_rule") == "custom" - and cr.custom.resource_key == resource_key - ): - return cr + return source_device_ep_info, destination_device_ep_info -def get_running_candidate_ietf_slice_data_diff(service_config: ServiceConfig) -> Dict: - running_ietf_slice_cr = get_custom_config_rule(service_config, RUNNING_RESOURCE_KEY) - running_resource_value_dict = json.loads( - running_ietf_slice_cr.custom.resource_value - ) - candidate_ietf_slice_cr = get_custom_config_rule( - service_config, CANDIDATE_RESOURCE_KEY - ) - candidate_resource_value_dict = json.loads( - candidate_ietf_slice_cr.custom.resource_value - ) - return DeepDiff( - running_resource_value_dict, - candidate_resource_value_dict, - ) +def _parse_item_added(diff: Dict) -> dict: + """ + Helper to parse 'iterable_item_added' from the running_candidate_diff + and return the relevant items for sdp, connection_group, match_criterion, etc. + """ + added_items = { + "sdp": {"sdp_idx": None, "value": {}}, + "connection_group": {"connection_group_idx": None, "value": {}}, + "match_criterion": { + "sdp_idx": None, + "match_criterion_idx": None, + "value": {}, + }, + } + for added_key, added_value in diff["iterable_item_added"].items(): + sdp_match = SDP_DIFF_RE.match(added_key) + connection_group_match = CONNECTION_GROUP_DIFF_RE.match(added_key) + match_criterion_match = MATCH_CRITERION_DIFF_RE.match(added_key) + if sdp_match: + added_items["sdp"] = { + "sdp_idx": int(sdp_match.groups()[0]), + "value": added_value, + } + elif connection_group_match: + added_items["connection_group"] = { + "connection_group_idx": int(connection_group_match.groups()[0]), + "value": added_value, + } + elif match_criterion_match: + added_items["match_criterion"] = { + "sdp_idx": int(match_criterion_match.groups()[0]), + "match_criterion_idx": int(match_criterion_match.groups()[1]), + "value": added_value, + } + return added_items class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): @@ -326,12 +401,15 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): results = [] try: service_config = self.__service.service_config + + # 1. Identify source and destination devices src_device_uuid, src_endpoint_uuid = get_device_endpoint_uuids(endpoints[0]) src_device_obj = self.__task_executor.get_device( DeviceId(**json_device_id(src_device_uuid)) ) src_device_name = src_device_obj.name src_controller = self.__task_executor.get_device_controller(src_device_obj) + dst_device_uuid, dst_endpoint_uuid = get_device_endpoint_uuids( endpoints[-1] ) @@ -340,18 +418,16 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ) dst_device_name = dst_device_obj.name dst_controller = self.__task_executor.get_device_controller(dst_device_obj) + if ( src_controller.device_id.device_uuid.uuid != dst_controller.device_id.device_uuid.uuid ): raise Exception("Different Src-Dst devices not supported by now") - controller = src_controller - context_client = ContextClient() - edge_device_names = [src_device_name, dst_device_name] - link_list = context_client.ListLinks(Empty()) - links = link_list.links - device_ep_pairs = [] - sdp_ids = [] + + controller = src_controller # same device controller + + # 2. Determine how the candidate & running resources differ running_candidate_diff = get_running_candidate_ietf_slice_data_diff( service_config ) @@ -370,35 +446,39 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): slice_name = running_resource_value_dict["network-slice-services"][ "slice-service" ][0]["id"] + + # 3. Retrieve the context links for matching endpoints + context_client = ContextClient() + links = context_client.ListLinks(Empty()).links + + device_ep_pairs = [] + sdp_ids = [] + target_connection_group_id = None + operation_type = "update" # default fallback + + # 4. Handle creation vs additions vs removals if not running_candidate_diff: # Slice Creation - slice_services = candidate_resource_value_dict[ + # 4a. New Slice Creation + operation_type = "create" + + candidate_slice_service = candidate_resource_value_dict[ "network-slice-services" - ]["slice-service"] - candidate_slice_service = slice_services[0] + ]["slice-service"][0] full_connection_groups = candidate_slice_service["connection-groups"][ "connection-group" ] sdps = candidate_slice_service["sdps"]["sdp"] - operation_type = "create" sdp_ids = [sdp["node-id"] for sdp in sdps] + + # figure out which device is connected to which link + edge_device_names = [src_device_name, dst_device_name] for sdp in sdps: node_id = sdp["node-id"] for link in links: - ( - device_obj_name_1, - ep_name_1, - device_obj_1, - device_obj_name_2, - ep_name_2, - device_obj_2, - ) = get_link_ep_device_names(link, context_client) - if ( - device_obj_name_1 == node_id - and device_obj_name_2 in edge_device_names - ): - del edge_device_names[ - edge_device_names.index(device_obj_name_2) - ] + info = get_link_ep_device_names(link, context_client) + dev1, ep1, _, dev2, ep2, _ = info + if dev1 == node_id and dev2 in edge_device_names: + edge_device_names.remove(dev2) match_criteria = sdp["service-match-criteria"][ "match-criterion" ] @@ -413,9 +493,9 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): device_ep_pairs.append( ( node_id, - ep_name_1, - device_obj_name_2, - ep_name_2, + ep1, + dev2, + ep2, sdp["id"], ipv4_info, ) @@ -423,19 +503,19 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): target_connection_group_id = match_criterion[ "target-connection-group-id" ] - del sdp_ids[sdp_ids.index(node_id)] + sdp_ids.remove(node_id) break + + # find the second link + if not edge_device_names: + raise Exception("Edge device names exhausted unexpectedly.") + + # second link logic for link in links: - ( - device_obj_name_1, - ep_name_1, - device_obj_1, - device_obj_name_2, - ep_name_2, - device_obj_2, - ) = get_link_ep_device_names(link, context_client) + info = get_link_ep_device_names(link, context_client) + dev1, ep1, device_obj_1, dev2, ep2, device_obj_2 = info if ( - device_obj_name_1 == edge_device_names[0] + dev1 == edge_device_names[0] and device_obj_2.controller_id != device_obj_1.controller_id ): for sdp in sdps: @@ -454,62 +534,34 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ) device_ep_pairs.append( ( - device_obj_name_2, - ep_name_2, - device_obj_name_1, - ep_name_1, + dev2, + ep2, + dev1, + ep1, sdp["id"], ipv4_info, ) ) + break + else: + raise Exception("No matching sdp found for second link.") break else: raise Exception("sdp between the domains not found") + elif "iterable_item_added" in running_candidate_diff: # new SDP added - slice_services = candidate_resource_value_dict[ + # 4b. A new SDP was added + operation_type = "update" + + candidate_slice_service = candidate_resource_value_dict[ "network-slice-services" - ]["slice-service"] - candidate_slice_service = slice_services[0] + ]["slice-service"][0] + sdps = candidate_slice_service["sdps"]["sdp"] full_connection_groups = candidate_slice_service["connection-groups"][ "connection-group" ] - sdps = candidate_slice_service["sdps"]["sdp"] - operation_type = "update" - added_items = { - "sdp": {"sdp_idx": None, "value": {}}, - "connection_group": {"connection_group_idx": None, "value": {}}, - "match_criterion": { - "sdp_idx": None, - "match_criterion_idx": None, - "value": {}, - }, - } - for added_key, added_value in running_candidate_diff[ - "iterable_item_added" - ].items(): - sdp_match = SDP_DIFF_RE.match(added_key) - connection_group_match = CONNECTION_GROUP_DIFF_RE.match(added_key) - match_criterion_match = MATCH_CRITERION_DIFF_RE.match(added_key) - if sdp_match: - added_items["sdp"] = { - "sdp_idx": int(sdp_match.groups()[0]), - "value": added_value, - } - elif connection_group_match: - added_items["connection_group"] = { - "connection_group_idx": int( - connection_group_match.groups()[0] - ), - "value": added_value, - } - elif match_criterion_match: - added_items["match_criterion"] = { - "sdp_idx": int(match_criterion_match.groups()[0]), - "match_criterion_idx": int( - match_criterion_match.groups()[1] - ), - "value": added_value, - } + added_items = _parse_item_added(running_candidate_diff) + new_sdp = sdps[added_items["sdp"]["sdp_idx"]] src_sdp_name = new_sdp["node-id"] dst_sdp_idx = sdps[added_items["match_criterion"]["sdp_idx"]]["id"] @@ -517,16 +569,10 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): "node-id" ] for link in links: - ( - device_obj_name_1, - ep_name_1, - device_obj_1, - device_obj_name_2, - ep_name_2, - device_obj_2, - ) = get_link_ep_device_names(link, context_client) + info = get_link_ep_device_names(link, context_client) + dev1, ep1, device_obj_1, dev2, ep2, device_obj_2 = info if ( - device_obj_name_1 == src_sdp_name + dev1 == src_sdp_name and device_obj_2.controller_id != device_obj_1.controller_id ): for sdp in sdps: @@ -545,10 +591,10 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ) device_ep_pairs.append( ( - device_obj_name_2, - ep_name_2, - device_obj_name_1, - ep_name_1, + dev2, + ep2, + dev1, + ep1, sdp["id"], ipv4_info, ) @@ -560,16 +606,10 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): else: raise Exception("sdp between the domains not found") for link in links: - ( - device_obj_name_1, - ep_name_1, - device_obj_1, - device_obj_name_2, - ep_name_2, - device_obj_2, - ) = get_link_ep_device_names(link, context_client) + info = get_link_ep_device_names(link, context_client) + dev1, ep1, device_obj_1, dev2, ep2, device_obj_2 = info if ( - device_obj_name_1 == dst_sdp_name + dev1 == dst_sdp_name and device_obj_2.controller_id != device_obj_1.controller_id ): for sdp in sdps: @@ -598,10 +638,10 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ) device_ep_pairs.append( ( - device_obj_name_2, - ep_name_2, - device_obj_name_1, - ep_name_1, + dev2, + ep2, + dev1, + ep1, sdp["id"], ipv4_info, ) @@ -610,6 +650,9 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): else: raise Exception("sdp between the domains not found") elif "iterable_item_removed" in running_candidate_diff: # an SDP removed + # 4c. An existing SDP was removed + operation_type = "update" + slice_services = running_resource_value_dict["network-slice-services"][ "slice-service" ] @@ -622,7 +665,6 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): "connection-group" ] sdps = slice_service["sdps"]["sdp"] - operation_type = "update" removed_items = get_removed_items( candidate_resource_value_dict, running_resource_value_dict ) @@ -725,10 +767,15 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): break else: raise Exception("sdp between the domains not found") + else: + raise Exception( + "transition from candidate to running info not supported" + ) + candidate_connection_groups = candidate_slice_service["connection-groups"][ "connection-group" ] - LOGGER.debug(f"connection_groups: {candidate_connection_groups}") + if ( len( candidate_resource_value_dict["network-slice-services"][ @@ -737,12 +784,19 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ) == 0 ): + # 5. If connection_groups is now empty => operation = delete operation_type = "delete" + + # 6. Retrieve actual target connection_group from the full connection groups + if not target_connection_group_id: + raise Exception("No target_connection_group_id found.") target_connection_group = next( cg for cg in full_connection_groups if cg["id"] == target_connection_group_id ) + + # 7. Build source/destination device info source_device_ep_info, destination_device_ep_info = ( extract_source_destination_device_endpoint_info( device_ep_pairs, @@ -799,10 +853,13 @@ class L3NMSliceIETFSliceServiceHandler(_ServiceHandler): ], "slice_id": slice_name, } + + # 9. Create config rules and configure device json_config_rules = setup_config_rules(slice_name, resource_value_dict) del controller.device_config.config_rules[:] for jcr in json_config_rules: controller.device_config.config_rules.append(ConfigRule(**jcr)) + self.__task_executor.configure_device(controller) except Exception as e: # pylint: disable=broad-except raise e diff --git a/src/service/service/service_handlers/l3slice_ietfslice/__init__.py b/src/service/service/service_handlers/l3slice_ietfslice/__init__.py index 53d5157f750bfb085125cbd33faff1cec5924e14..3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460 100644 --- a/src/service/service/service_handlers/l3slice_ietfslice/__init__.py +++ b/src/service/service/service_handlers/l3slice_ietfslice/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# 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.