Commit 4c887249 authored by Shayan Hajipour's avatar Shayan Hajipour
Browse files

refactoring

parent eefc016a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
# 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.
+162 −110
Original line number Diff line number Diff line
# 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.
@@ -15,7 +15,7 @@
import json
import logging
import re
from typing import Any, List, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple, Union, TypedDict, Dict
from uuid import uuid4

from deepdiff import DeepDiff
@@ -40,6 +40,7 @@ CANDIDATE_RESOURCE_KEY = "candidate_ietf_slice"
LOGGER = logging.getLogger(__name__)

METRICS_POOL = MetricsPool("Service", "Handler", labels={"handler": "l3nm_nce"})

SDP_DIFF_RE = re.compile(
    r"^root\[\'network-slice-services\'\]\[\'slice-service\'\]\[0\]\[\'sdps\'\]\[\'sdp\'\]\[(\d)\]$"
)
@@ -51,9 +52,20 @@ MATCH_CRITERION_DIFF_RE = re.compile(
)


class Ipv4Info(TypedDict):
    src_ip: str
    dst_ip: str
    src_port: str
    dst_port: str


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": {}},
@@ -63,20 +75,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
@@ -88,7 +104,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"]
@@ -99,6 +115,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
@@ -119,41 +136,116 @@ def get_removed_items(
        or removed_items["connection_group"]["connection_group_idx"] is None
    ):
        raise Exception("sdp, connection group or match criterion not found")

    return removed_items


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


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
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_qos_info(
    connection_groups: List, connection_grp_id: str, src_sdp_idx: str, dst_sdp_idx: str
) -> Dict:
    """
    Extract QoS information from connection groups based on the connection group ID.
    """
    qos_info = {
        "upstream": {"max_delay": "0", "bw": "0", "packet_loss": "0"},
        "downstream": {"max_delay": "0", "bw": "0", "packet_loss": "0"},
    }
    connection_group = next(
        (cg for cg in connection_groups if cg["id"] == connection_grp_id), None
    )
    return (
        DeepDiff(
            running_resource_value_dict,
            candidate_resource_value_dict,
        ),
        DeepDiff(
            running_resource_value_dict,
            candidate_resource_value_dict,
            ignore_order=True,
        ),

    if not connection_group:
        return qos_info

    for cc in connection_group["connectivity-construct"]:
        if (
            cc["p2p-sender-sdp"] == src_sdp_idx
            and cc["p2p-receiver-sdp"] == dst_sdp_idx
        ):
            direction = "upstream"
        elif (
            cc["p2p-sender-sdp"] == dst_sdp_idx
            and cc["p2p-receiver-sdp"] == src_sdp_idx
        ):
            direction = "downstream"
        else:
            raise Exception("invalid sender and receiver sdp ids")
        for metric_bound in cc["service-slo-sle-policy"]["slo-policy"]["metric-bound"]:
            if (
                metric_bound["metric-type"]
                == "ietf-network-slice-service:one-way-delay-maximum"
                and metric_bound["metric-unit"] == "milliseconds"
            ):
                qos_info[direction]["max_delay"] = metric_bound["bound"]
            elif (
                metric_bound["metric-type"]
                == "ietf-network-slice-service:one-way-bandwidth"
                and metric_bound["metric-unit"] == "Mbps"
            ):
                qos_info[direction]["bw"] = metric_bound["bound"]
            elif (
                metric_bound["metric-type"]
                == "ietf-network-slice-service:two-way-packet-loss"
                and metric_bound["metric-unit"] == "percentage"
            ):
                qos_info[direction]["packet_loss"] = metric_bound["percentile-value"]

    return qos_info


def extract_match_criterion_ipv4_info(match_criterion: Dict) -> Ipv4Info:
    """
    Extracts IPv4 info from the match criterion dictionary.
    """
    src_ip = dst_ip = src_port = dst_port = ""

    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_ip = val.split("/")[0]
        elif m_type == "ietf-network-slice-service:destination-ip-prefix":
            dst_ip = val.split("/")[0]
        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

    return Ipv4Info(
        src_ip=src_ip,
        dst_ip=dst_ip,
        src_port=src_port,
        dst_port=dst_port,
    )


@@ -174,22 +266,25 @@ class L3NMNCEServiceHandler(_ServiceHandler):
        chk_type("endpoints", endpoints, list)
        if len(endpoints) == 0:
            return []

        results = []
        try:
            context_client = ContextClient()
            service_config = self.__service.service_config
            settings = self.__settings_handler.get("/settings")

            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))
            )
            controller = self.__task_executor.get_device_controller(src_device_obj)

            list_devices = context_client.ListDevices(Empty())
            devices = list_devices.devices
            device_name_device = {d.name: d for d in devices}
            device_uuid_device = {d.device_id.device_uuid.uuid: d for d in devices}
            running_candidate_diff, running_candidate_diff_no_order = (
                get_running_candidate_ietf_slice_data_diff(service_config)
            device_name_map = {d.name: d for d in devices}

            running_candidate_diff = get_running_candidate_ietf_slice_data_diff(
                service_config
            )
            candidate_ietf_slice_cr = get_custom_config_rule(
                service_config, CANDIDATE_RESOURCE_KEY
@@ -203,15 +298,17 @@ class L3NMNCEServiceHandler(_ServiceHandler):
            running_resource_value_dict = json.loads(
                running_ietf_slice_cr.custom.resource_value
            )

            service_name = running_resource_value_dict["network-slice-services"][
                "slice-service"
            ][0]["id"]

            if not running_candidate_diff:  # Slice Creation
                operation_type = "create"
                slice_services = candidate_resource_value_dict[
                    "network-slice-services"
                ]["slice-service"]
                slice_service = slice_services[0]

                slice_service = candidate_resource_value_dict["network-slice-services"][
                    "slice-service"
                ][0]
                sdps = slice_service["sdps"]["sdp"]
                connection_groups = slice_service["connection-groups"][
                    "connection-group"
@@ -219,7 +316,7 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                sdp_ids = [sdp["id"] for sdp in sdps]
                for sdp in sdps:
                    node_id = sdp["node-id"]
                    device_obj = device_name_device[node_id]
                    device_obj = device_name_map[node_id]
                    device_controller = self.__task_executor.get_device_controller(
                        device_obj
                    )
@@ -237,15 +334,15 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                else:
                    raise Exception("connection group id not found")
            elif "iterable_item_added" in running_candidate_diff:  # new SDP added
                slice_services = candidate_resource_value_dict[
                    "network-slice-services"
                ]["slice-service"]
                slice_service = slice_services[0]
                operation_type = "create"

                slice_service = candidate_resource_value_dict["network-slice-services"][
                    "slice-service"
                ][0]
                sdps = slice_service["sdps"]["sdp"]
                connection_groups = slice_service["connection-groups"][
                    "connection-group"
                ]
                operation_type = "create"
                added_items = {
                    "sdp": {"sdp_idx": None, "value": {}},
                    "connection_group": {"connection_group_idx": None, "value": {}},
@@ -287,6 +384,7 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                connection_grp_id = connection_groups[
                    added_items["connection_group"]["connection_group_idx"]
                ]["id"]

                if (
                    connection_grp_id
                    != added_items["match_criterion"]["value"][
@@ -299,15 +397,15 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                match_criteria = new_sdp["service-match-criteria"]["match-criterion"]
                match_criterion = match_criteria[0]
            elif "iterable_item_removed" in running_candidate_diff:  # new SDP added
                slice_services = running_resource_value_dict["network-slice-services"][
                operation_type = "delete"

                slice_service = running_resource_value_dict["network-slice-services"][
                    "slice-service"
                ]
                slice_service = slice_services[0]
                ][0]
                sdps = slice_service["sdps"]["sdp"]
                connection_groups = slice_service["connection-groups"][
                    "connection-group"
                ]
                operation_type = "delete"
                removed_items = get_removed_items(
                    candidate_resource_value_dict, running_resource_value_dict
                )
@@ -317,6 +415,7 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                connection_grp_id = connection_groups[
                    removed_items["connection_group"]["connection_group_idx"]
                ]["id"]

                if (
                    connection_grp_id
                    != removed_items["match_criterion"]["value"][
@@ -330,65 +429,17 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                    "match-criterion"
                ]
                match_criterion = match_criteria[0]
            for type_value in match_criterion["match-type"]:
                if type_value["type"] == "ietf-network-slice-service:source-ip-prefix":
                    src_ip = type_value["value"][0].split("/")[0]
                elif (
                    type_value["type"]
                    == "ietf-network-slice-service:destination-ip-prefix"
                ):
                    dst_ip = type_value["value"][0].split("/")[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]
            qos_info = {
                "upstream": {"max_delay": "0", "bw": "0", "packet_loss": "0"},
                "downstream": {"max_delay": "0", "bw": "0", "packet_loss": "0"},
            }
            for cg in connection_groups:
                if cg["id"] != connection_grp_id:
                    continue
                for cc in cg["connectivity-construct"]:
                    if (
                        cc["p2p-sender-sdp"] == src_sdp_idx
                        and cc["p2p-receiver-sdp"] == dst_sdp_idx
                    ):
                        direction = "upstream"
                    elif (
                        cc["p2p-sender-sdp"] == dst_sdp_idx
                        and cc["p2p-receiver-sdp"] == src_sdp_idx
                    ):
                        direction = "downstream"
            else:
                        raise Exception("invalid sender and receiver sdp ids")
                    for metric_bound in cc["service-slo-sle-policy"]["slo-policy"][
                        "metric-bound"
                    ]:
                        if (
                            metric_bound["metric-type"]
                            == "ietf-network-slice-service:one-way-delay-maximum"
                            and metric_bound["metric-unit"] == "milliseconds"
                        ):
                            qos_info[direction]["max_delay"] = metric_bound["bound"]
                        elif (
                            metric_bound["metric-type"]
                            == "ietf-network-slice-service:one-way-bandwidth"
                            and metric_bound["metric-unit"] == "Mbps"
                        ):
                            qos_info[direction]["bw"] = metric_bound["bound"]
                        elif (
                            metric_bound["metric-type"]
                            == "ietf-network-slice-service:two-way-packet-loss"
                            and metric_bound["metric-unit"] == "percentage"
                        ):
                            qos_info[direction]["packet_loss"] = metric_bound[
                                "percentile-value"
                            ]
                break
                raise Exception(
                    "transition from candidate to running info not supported"
                )

            ip_info = extract_match_criterion_ipv4_info(match_criterion)

            qos_info = extract_qos_info(
                connection_groups, connection_grp_id, src_sdp_idx, dst_sdp_idx
            )

            resource_value_dict = {
                "uuid": service_name,
                "operation_type": operation_type,
@@ -401,22 +452,23 @@ class L3NMNCEServiceHandler(_ServiceHandler):
                "upstream_max_bw": 2 * int(qos_info["upstream"]["bw"]) * 1e6,
                "downstream_assure_bw": int(qos_info["downstream"]["bw"]) * 1e6,
                "downstream_max_bw": 2 * int(qos_info["downstream"]["bw"]) * 1e6,
                "src_ip": src_ip,
                "src_port": src_port,
                "dst_ip": dst_ip,
                "dst_port": dst_port,
                "src_ip": ip_info["src_ip"],
                "src_port": ip_info["src_port"],
                "dst_ip": ip_info["dst_ip"],
                "dst_port": ip_info["dst_port"],
            }
            json_config_rules = setup_config_rules(service_name, resource_value_dict)
            LOGGER.debug(f"Config Rules: {json_config_rules}")

            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)
            LOGGER.debug('Configured device "{:s}"'.format(controller.name))

        except Exception as e:  # pylint: disable=broad-except
            LOGGER.exception(f"P4: {e}")
            raise e
            results.append(e)

        return results

    @metered_subclass_method(METRICS_POOL)
+1 −1
Original line number Diff line number Diff line
# 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.