Skip to content
Snippets Groups Projects
Commit 97d1a99d authored by Shayan Hajipour's avatar Shayan Hajipour
Browse files

Merge branch 'feat/241-cttc-l3nm-nce-service-handler-is-required' into camara-demo-integration

parents d5d450dd 4c887249
No related branches found
No related tags found
3 merge requests!346Draft: support for restconf protocol,!345Draft: support ipinfusion devices via netconf,!321Resolve: "(CTTC) CAMARA Demo Integration tests"
# 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.
......
# 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
else:
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)
......
# 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.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment