diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network_slice/ietf_slice_handler.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network_slice/ietf_slice_handler.py index 46e2423c4a12ae785cb8bc610f9d76dea03ee662..6c52a43988d041048c2d3f5efa927e0f5a90284b 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_network_slice/ietf_slice_handler.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network_slice/ietf_slice_handler.py @@ -32,90 +32,177 @@ ADDRESS_PREFIX = 24 RAISE_IF_DIFFERS = False -def get_endpoint_controller_type( - endpoint: EndPointId, context_client: ContextClient -) -> str: - endpoint_device: Device = context_client.GetDevice(endpoint.device_id) - if endpoint_device.controller_id == DeviceId(): - return "" - controller = context_client.GetDevice(endpoint_device.controller_id) - if controller is None: - controller_uuid = endpoint_device.controller_id.device_uuid.uuid - raise Exception("Device({:s}) not found".format(str(controller_uuid))) - return controller.device_type +def validate_ietf_slice_data(request_data: Dict) -> None: + """ + Validate the provided IETF slice data against the YANG model. + """ + yang_validator = YangValidator("ietf-network-slice-service") + _ = yang_validator.parse_to_dict(request_data) + yang_validator.destroy() def get_custom_config_rule( service_config: ServiceConfig, resource_key: str ) -> Optional[ConfigRule]: + """ + Retrieve the custom config rule with the given resource_key from a ServiceConfig. + """ 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_ietf_data_from_config(slice_request: Slice, resource_key: str) -> Dict: + """ + Retrieve the IETF data (as a Python dict) from a slice's config rule for the specified resource_key. + Raises an exception if not found. + """ + config_rule = get_custom_config_rule(slice_request.slice_config, resource_key) + if not config_rule: + raise Exception(f"IETF data not found for resource_key: {resource_key}") + return json.loads(config_rule.custom.resource_value) + + +def update_ietf_data_in_config( + slice_request: Slice, resource_key: str, ietf_data: Dict +) -> None: + """ + Update the slice config rule (identified by resource_key) with the provided IETF data. + """ + fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} + update_config_rule_custom( + slice_request.slice_config.config_rules, resource_key, fields + ) + + +def build_constraints_from_connection_group(connection_group: dict) -> List[Constraint]: + """ + Build a list of Constraints from the 'metric-bound' data in a connection group. + """ + constraints = [] + metric_bounds = connection_group["connectivity-construct"][0][ + "service-slo-sle-policy" + ]["slo-policy"]["metric-bound"] + + for metric in metric_bounds: + metric_type = metric["metric-type"] + if metric_type == "ietf-nss:one-way-delay-maximum": + bound_value = float(metric["bound"]) + constraint = Constraint() + constraint.sla_latency.e2e_latency_ms = bound_value + constraints.append(constraint) + elif metric_type == "ietf-nss:one-way-bandwidth": + bound_value = float(metric["bound"]) + constraint = Constraint() + # Convert from Mbps to Gbps if needed + constraint.sla_capacity.capacity_gbps = bound_value / 1.0e3 + constraints.append(constraint) + + return constraints + + +def get_endpoint_controller_type( + endpoint: EndPointId, context_client: ContextClient +) -> str: + """ + Retrieve the device type of an endpoint's controller device, if any; otherwise returns an empty string. + """ + endpoint_device: Device = context_client.GetDevice(endpoint.device_id) + if endpoint_device.controller_id == DeviceId(): + return "" + controller = context_client.GetDevice(endpoint_device.controller_id) + if controller is None: + controller_uuid = endpoint_device.controller_id.device_uuid.uuid + raise Exception(f"Controller device {controller_uuid} not found") + return controller.device_type def sort_endpoints( - endpoinst_list: List[EndPointId], + endpoints_list: List[EndPointId], sdps: List, connection_group: Dict, context_client: ContextClient, ) -> List[EndPointId]: - first_ep = endpoinst_list[0] + """ + Sort the endpoints_list based on controller type: + - If the first endpoint is an NCE, keep order. + - If the last endpoint is an NCE, reverse order. + - Otherwise, use the 'p2p-sender-sdp' from the connection group to decide. + """ + if not endpoints_list: + return endpoints_list + + first_ep = endpoints_list[0] + last_ep = endpoints_list[-1] first_controller_type = get_endpoint_controller_type(first_ep, context_client) - last_ep = endpoinst_list[-1] last_controller_type = get_endpoint_controller_type(last_ep, context_client) + if first_controller_type == DeviceTypeEnum.NCE.value: - return endpoinst_list + return endpoints_list elif last_controller_type == DeviceTypeEnum.NCE.value: - return endpoinst_list[::-1] - else: - src_sdp_id = connection_group["connectivity-construct"][0]["p2p-sender-sdp"] - sdp_id_name_mapping = {sdp["id"]: sdp["node-id"] for sdp in sdps} - if ( - endpoinst_list[0].device_id.device_uuid.uuid - == sdp_id_name_mapping[src_sdp_id] - ): - return endpoinst_list - return endpoinst_list[::-1] + return endpoints_list[::-1] + + src_sdp_id = connection_group["connectivity-construct"][0]["p2p-sender-sdp"] + sdp_id_name_mapping = {sdp["id"]: sdp["node-id"] for sdp in sdps} + if endpoints_list[0].device_id.device_uuid.uuid == sdp_id_name_mapping[src_sdp_id]: + return endpoints_list + return endpoints_list[::-1] def replace_ont_endpoint_with_emu_dc( - endpoint_list: List, context_client: ContextClient -) -> List: + endpoint_list: List[EndPointId], context_client: ContextClient +) -> List[EndPointId]: + """ + Replace an ONT endpoint in endpoint_list with an 'emu-datacenter' endpoint if found. + One endpoint must be managed (controller_id != empty), the other must be unmanaged. + """ + if len(endpoint_list) != 2: + raise Exception( + "Expecting exactly two endpoints to handle ONT -> emu-dc replacement" + ) + link_list = context_client.ListLinks(Empty()) links = list(link_list.links) devices_list = context_client.ListDevices(Empty()) devices = devices_list.devices + uuid_name_map = {d.device_id.device_uuid.uuid: d.name for d in devices} uuid_device_map = {d.device_id.device_uuid.uuid: d for d in devices} name_device_map = {d.name: d for d in devices} - endpoint_id_1 = endpoint_list[0] + + endpoint_id_1, endpoint_id_2 = endpoint_list device_uuid_1 = endpoint_id_1.device_id.device_uuid.uuid - device_1 = name_device_map[device_uuid_1] - endpoint_id_2 = endpoint_list[1] device_uuid_2 = endpoint_id_2.device_id.device_uuid.uuid - device_2 = name_device_map[device_uuid_2] + + device_1 = name_device_map.get(device_uuid_1) + device_2 = name_device_map.get(device_uuid_2) + + if not device_1 or not device_2: + raise Exception("One or both devices not found in name_device_map") + + # Check if the first endpoint is managed if device_1.controller_id != DeviceId(): for link in links: link_endpoints = list(link.link_endpoint_ids) - link_ep_1 = link_endpoints[0] - link_ep_2 = link_endpoints[1] + link_ep_1, link_ep_2 = link_endpoints if ( - device_uuid_1 == uuid_name_map[link_ep_1.device_id.device_uuid.uuid] + device_uuid_1 == uuid_name_map.get(link_ep_1.device_id.device_uuid.uuid) and uuid_device_map[link_ep_2.device_id.device_uuid.uuid].device_type == "emu-datacenter" ): endpoint_list[0] = link_ep_2 break + # Otherwise, check if the second endpoint is managed elif device_2.controller_id != DeviceId(): for link in links: link_endpoints = list(link.link_endpoint_ids) - link_ep_1 = link_endpoints[0] - link_ep_2 = link_endpoints[1] + link_ep_1, link_ep_2 = link_endpoints if ( - device_uuid_2 == uuid_name_map[link_ep_1.device_id.device_uuid.uuid] + device_uuid_2 == uuid_name_map.get(link_ep_1.device_id.device_uuid.uuid) and uuid_device_map[link_ep_2.device_id.device_uuid.uuid].device_type == "emu-datacenter" ): @@ -123,31 +210,34 @@ def replace_ont_endpoint_with_emu_dc( break else: raise Exception( - "one of the sdps should be managed by a controller and the other one should not be controlled" + "One endpoint should be managed by a controller and the other should not be" ) - return endpoint_list - -def validate_ietf_slice_data(request_data: Dict) -> None: - yang_validator = YangValidator("ietf-network-slice-service") - _ = yang_validator.parse_to_dict(request_data) - yang_validator.destroy() + return endpoint_list class IETFSliceHandler: @staticmethod def get_all_ietf_slices(context_client: ContextClient) -> Dict: + """ + Retrieve all IETF slices from the (single) context. Expects exactly one context in the system. + """ existing_context_ids = context_client.ListContextIds(Empty()) context_ids = list(existing_context_ids.context_ids) if len(context_ids) != 1: - raise Exception("Number of contexts should be 1") + raise Exception("Number of contexts should be exactly 1") + slices_list = context_client.ListSlices(context_ids[0]) slices = slices_list.slices + ietf_slices = {"network-slice-services": {"slice-service": []}} - for slice in slices: + for slc in slices: candidate_cr = get_custom_config_rule( - slice.slice_config, CANDIDATE_RESOURCE_KEY + slc.slice_config, CANDIDATE_RESOURCE_KEY ) + if not candidate_cr: + # Skip slices that don't have the candidate_ietf_slice data + continue candidate_ietf_data = json.loads(candidate_cr.custom.resource_value) ietf_slices["network-slice-services"]["slice-service"].append( candidate_ietf_data["network-slice-services"]["slice-service"][0] @@ -158,27 +248,37 @@ class IETFSliceHandler: def create_slice_service( request_data: dict, context_client: ContextClient ) -> Slice: + """ + Create a new slice service from the provided IETF data, applying validations and constructing a Slice object. + """ + # Ensure the top-level key is "network-slice-services" if "network-slice-services" not in request_data: request_data = {"network-slice-services": request_data} + validate_ietf_slice_data(request_data) - slice_services = request_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + slice_service = request_data["network-slice-services"]["slice-service"][0] + slice_id = slice_service["id"] sdps = slice_service["sdps"]["sdp"] - connection_groups = slice_service["connection-groups"]["connection-group"] if len(sdps) != 2: - raise Exception("Number of SDPs should be 2") - slice_request: Slice = Slice() + raise Exception("Number of SDPs should be exactly 2") + + connection_groups = slice_service["connection-groups"]["connection-group"] + slice_request = Slice() slice_request.slice_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME slice_request.slice_id.slice_uuid.uuid = slice_id slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED + list_endpoints = [] endpoint_config_rules = [] connection_group_ids = set() + + # Build endpoints from SDPs for sdp in sdps: attachment_circuits = sdp["attachment-circuits"]["attachment-circuit"] if len(attachment_circuits) != 1: - raise Exception("All SDPs should have 1 attachment-circuit") + raise Exception("Each SDP must have exactly 1 attachment-circuit") + endpoint = EndPointId() endpoint.topology_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME device_uuid = sdp["node-id"] @@ -186,11 +286,15 @@ class IETFSliceHandler: endpoint_uuid = attachment_circuits[0]["ac-tp-id"] endpoint.endpoint_uuid.uuid = endpoint_uuid list_endpoints.append(endpoint) + + # Keep track of connection-group-id from each SDP connection_group_ids.add( sdp["service-match-criteria"]["match-criterion"][0][ "target-connection-group-id" ] ) + + # Endpoint-specific config rule fields endpoint_config_rule_fields = { "address_ip": (endpoint_uuid, RAISE_IF_DIFFERS), "address_prefix": (ADDRESS_PREFIX, RAISE_IF_DIFFERS), @@ -201,41 +305,36 @@ class IETFSliceHandler: endpoint_config_rule_fields, ) ) + if len(connection_group_ids) != 1: - raise Exception("SDPs target-connection-group-id do not match") - list_constraints = [] - for cg in connection_groups: - if cg["id"] != list(connection_group_ids)[0]: - continue - metric_bounds = cg["connectivity-construct"][0]["service-slo-sle-policy"][ - "slo-policy" - ]["metric-bound"] - for metric in metric_bounds: - if metric["metric-type"] == "ietf-nss:one-way-delay-maximum": - constraint = Constraint() - constraint.sla_latency.e2e_latency_ms = float(metric["bound"]) - list_constraints.append(constraint) - elif metric["metric-type"] == "ietf-nss:one-way-bandwidth": - constraint = Constraint() - constraint.sla_capacity.capacity_gbps = ( - float(metric["bound"]) / 1.0e3 - ) - list_constraints.append(constraint) - break - else: - raise Exception("connection group not found") - list_endpoints = sort_endpoints(list_endpoints, sdps, cg, context_client) + raise Exception("SDPs do not share a common connection-group-id") + + # Build constraints from the matching connection group + unique_cg_id = connection_group_ids.pop() + found_cg = next( + (cg for cg in connection_groups if cg["id"] == unique_cg_id), None + ) + if not found_cg: + raise Exception("The connection group referenced by the SDPs was not found") + + list_constraints = build_constraints_from_connection_group(found_cg) + + # Sort endpoints and optionally replace the ONT endpoint + list_endpoints = sort_endpoints(list_endpoints, sdps, found_cg, context_client) list_endpoints = replace_ont_endpoint_with_emu_dc( list_endpoints, context_client ) + slice_request.slice_endpoint_ids.extend(list_endpoints) slice_request.slice_constraints.extend(list_constraints) - # TODO adding owner, needs to be recoded after updating the bindings - owner = slice_id - slice_request.slice_owner.owner_string = owner + + # Set slice owner + slice_request.slice_owner.owner_string = slice_id slice_request.slice_owner.owner_uuid.uuid = str( - uuid.uuid5(uuid.NAMESPACE_DNS, owner) + uuid.uuid5(uuid.NAMESPACE_DNS, slice_id) ) + + # Update slice config with IETF data (both running and candidate) ietf_slice_fields = { name: (value, RAISE_IF_DIFFERS) for name, value in request_data.items() } @@ -250,6 +349,7 @@ class IETFSliceHandler: ietf_slice_fields, ) + # Update endpoint config rules for ep_cr_key, ep_cr_fields in endpoint_config_rules: update_config_rule_custom( slice_request.slice_config.config_rules, ep_cr_key, ep_cr_fields @@ -261,120 +361,113 @@ class IETFSliceHandler: def create_sdp( request_data: dict, slice_uuid: str, context_client: ContextClient ) -> Slice: + """ + Add a new SDP to an existing slice, updating the candidate IETF data. + """ sdps = request_data["sdp"] if len(sdps) != 1: - raise Exception("Number of SDPs should be 1") + raise Exception("Number of SDPs to create must be exactly 1") + new_sdp = sdps[0] - # slice_request = get_slice_by_uuid(context_client, slice_uuid) slice_request = get_slice_by_defualt_name( context_client, slice_uuid, rw_copy=False ) - for cr in slice_request.slice_config.config_rules: - if cr.WhichOneof("config_rule") != "custom": - continue - if cr.custom.resource_key == CANDIDATE_RESOURCE_KEY: - ietf_data = json.loads(cr.custom.resource_value) - break - else: - raise Exception("ietf data not found") - slice_services = ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + ietf_data = get_ietf_data_from_config(slice_request, CANDIDATE_RESOURCE_KEY) + + slice_service = ietf_data["network-slice-services"]["slice-service"][0] slice_sdps = slice_service["sdps"]["sdp"] slice_sdps.append(new_sdp) - fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields - ) + + # Save updated IETF data + update_ietf_data_in_config(slice_request, CANDIDATE_RESOURCE_KEY, ietf_data) return slice_request @staticmethod def delete_sdp( slice_uuid: str, sdp_id: str, context_client: ContextClient ) -> Slice: - # slice_request = get_slice_by_uuid(context_client, slice_uuid) + """ + Delete the specified SDP from an existing slice's candidate IETF data. + """ slice_request = get_slice_by_defualt_name( context_client, slice_uuid, rw_copy=False ) - for cr in slice_request.slice_config.config_rules: - if cr.WhichOneof("config_rule") != "custom": - continue - if cr.custom.resource_key == CANDIDATE_RESOURCE_KEY: - ietf_data = json.loads(cr.custom.resource_value) - break - else: - raise Exception("ietf data not found") - slice_services = ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + ietf_data = get_ietf_data_from_config(slice_request, CANDIDATE_RESOURCE_KEY) + + slice_service = ietf_data["network-slice-services"]["slice-service"][0] slice_sdps = slice_service["sdps"]["sdp"] - sdp_idx = list((slice_sdp["id"] == sdp_id for slice_sdp in slice_sdps)).index( - True + + # Find and remove the matching SDP + sdp_idx = next( + (i for i, sdp in enumerate(slice_sdps) if sdp["id"] == sdp_id), None ) + if sdp_idx is None: + raise Exception(f"SDP with id '{sdp_id}' not found in slice '{slice_uuid}'") slice_sdps.pop(sdp_idx) - fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields - ) + + update_ietf_data_in_config(slice_request, CANDIDATE_RESOURCE_KEY, ietf_data) return slice_request @staticmethod def create_connection_group( request_data: dict, slice_id: str, context_client: ContextClient ) -> Slice: + """ + Add a new connection group to an existing slice's candidate IETF data. + """ connection_groups = request_data["connection-group"] if len(connection_groups) != 1: - raise Exception("Number of connection groups should be 1") + raise Exception("Number of connection groups to create must be exactly 1") + new_connection_group = connection_groups[0] - # slice = get_slice_by_uuid(context_client, slice_id) - slice = get_slice_by_defualt_name(context_client, slice_id, rw_copy=False) - for cr in slice.slice_config.config_rules: - if cr.WhichOneof("config_rule") != "custom": - continue - if cr.custom.resource_key == CANDIDATE_RESOURCE_KEY: - ietf_data = json.loads(cr.custom.resource_value) - break - else: - raise Exception("ietf data not found") - slice_services = ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + slice_request = get_slice_by_defualt_name( + context_client, slice_id, rw_copy=False + ) + ietf_data = get_ietf_data_from_config(slice_request, CANDIDATE_RESOURCE_KEY) + + slice_service = ietf_data["network-slice-services"]["slice-service"][0] slice_connection_groups = slice_service["connection-groups"]["connection-group"] slice_connection_groups.append(new_connection_group) - fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} - update_config_rule_custom( - slice.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields - ) + + # Validate the updated data, then save validate_ietf_slice_data(ietf_data) - return slice + update_ietf_data_in_config(slice_request, CANDIDATE_RESOURCE_KEY, ietf_data) + return slice_request @staticmethod def update_connection_group( slice_name: str, updated_connection_group: dict, context_client: ContextClient, - ): + ) -> Slice: + """ + Update an existing connection group in the candidate IETF data. + """ slice_request = get_slice_by_defualt_name( context_client, slice_name, rw_copy=False ) - slice_config = slice_request.slice_config - cr = get_custom_config_rule(slice_config, CANDIDATE_RESOURCE_KEY) - candidate_ietf_data = json.loads(cr.custom.resource_value) - slice_services = candidate_ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + candidate_ietf_data = get_ietf_data_from_config( + slice_request, CANDIDATE_RESOURCE_KEY + ) + + slice_service = candidate_ietf_data["network-slice-services"]["slice-service"][ + 0 + ] slice_connection_groups = slice_service["connection-groups"]["connection-group"] - connection_group_id = updated_connection_group["id"] - cg_idx = list( - ( - slice_cg["id"] == connection_group_id - for slice_cg in slice_connection_groups - ) - ).index(True) + + cg_id = updated_connection_group["id"] + cg_idx = next( + (i for i, cg in enumerate(slice_connection_groups) if cg["id"] == cg_id), + None, + ) + if cg_idx is None: + raise Exception(f"Connection group with id '{cg_id}' not found") + slice_connection_groups[cg_idx] = updated_connection_group - fields = { - name: (value, RAISE_IF_DIFFERS) - for name, value in candidate_ietf_data.items() - } - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields + update_ietf_data_in_config( + slice_request, CANDIDATE_RESOURCE_KEY, candidate_ietf_data ) + slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED return slice_request @@ -382,30 +475,39 @@ class IETFSliceHandler: def delete_connection_group( slice_uuid: str, connection_group_id: str, context_client: ContextClient ) -> Slice: - # slice_request = get_slice_by_uuid(context_client, slice_uuid) + """ + Remove an existing connection group from the candidate IETF data of a slice. + """ slice_request = get_slice_by_defualt_name( context_client, slice_uuid, rw_copy=False ) - slice_config = slice_request.slice_config - cr = get_custom_config_rule(slice_config, CANDIDATE_RESOURCE_KEY) - candidate_ietf_data = json.loads(cr.custom.resource_value) - slice_services = candidate_ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + candidate_ietf_data = get_ietf_data_from_config( + slice_request, CANDIDATE_RESOURCE_KEY + ) + + slice_service = candidate_ietf_data["network-slice-services"]["slice-service"][ + 0 + ] slice_connection_groups = slice_service["connection-groups"]["connection-group"] - sdp_idx = list( + + cg_idx = next( ( - slice_cr["id"] == connection_group_id - for slice_cr in slice_connection_groups + i + for i, cg in enumerate(slice_connection_groups) + if cg["id"] == connection_group_id + ), + None, + ) + if cg_idx is None: + raise Exception( + f"Connection group with id '{connection_group_id}' not found" ) - ).index(True) - removed_connection_group = slice_connection_groups.pop(sdp_idx) - fields = { - name: (value, RAISE_IF_DIFFERS) - for name, value in candidate_ietf_data.items() - } - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields + + slice_connection_groups.pop(cg_idx) + update_ietf_data_in_config( + slice_request, CANDIDATE_RESOURCE_KEY, candidate_ietf_data ) + slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED return slice_request @@ -413,91 +515,55 @@ class IETFSliceHandler: def create_match_criteria( request_data: dict, slice_name: str, sdp_id: str, context_client: ContextClient ) -> Slice: + """ + Create a new match-criterion for the specified SDP in a slice's candidate IETF data. + """ match_criteria = request_data["match-criterion"] if len(match_criteria) != 1: - raise Exception("Number of SDPs should be 1") + raise Exception( + "Number of match-criterion entries to create must be exactly 1" + ) + new_match_criterion = match_criteria[0] target_connection_group_id = new_match_criterion["target-connection-group-id"] - # slice_request = get_slice_by_uuid(context_client, slice_id) + slice_request = get_slice_by_defualt_name( context_client, slice_name, rw_copy=False ) - for cr in slice_request.slice_config.config_rules: - if cr.WhichOneof("config_rule") != "custom": - continue - if cr.custom.resource_key == CANDIDATE_RESOURCE_KEY: - ietf_data = json.loads(cr.custom.resource_value) - break - else: - raise Exception("ietf data not found") - slice_services = ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] - slice_id = slice_service["id"] - sdps = slice_service["sdps"]["sdp"] + ietf_data = get_ietf_data_from_config(slice_request, CANDIDATE_RESOURCE_KEY) + + slice_service = ietf_data["network-slice-services"]["slice-service"][0] connection_groups = slice_service["connection-groups"]["connection-group"] - slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED - list_endpoints = [] - for sdp in sdps: - if ( - sdp["service-match-criteria"]["match-criterion"][0][ - "target-connection-group-id" - ] - == target_connection_group_id - ): - attachment_circuits = sdp["attachment-circuits"]["attachment-circuit"] - if len(attachment_circuits) != 1: - raise Exception("All SDPs should have 1 attachment-circuit") - endpoint = EndPointId() - endpoint.topology_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME - endpoint.device_id.device_uuid.uuid = sdp["node-id"] - endpoint.endpoint_uuid.uuid = attachment_circuits[0]["ac-tp-id"] - list_endpoints.append(endpoint) - break - else: - raise Exception("Second SDP not found") - for sdp in sdps: - if sdp["id"] == sdp_id: - sdp["service-match-criteria"]["match-criterion"].append( - new_match_criterion - ) - attachment_circuits = sdp["attachment-circuits"]["attachment-circuit"] - if len(attachment_circuits) != 1: - raise Exception("All SDPs should have 1 attachment-circuit") - endpoint = EndPointId() - endpoint.topology_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME - endpoint.device_id.device_uuid.uuid = sdp["node-id"] - endpoint.endpoint_uuid.uuid = attachment_circuits[0]["ac-tp-id"] - list_endpoints.append(endpoint) - break - else: - raise Exception("SDP not found") - list_constraints = [] - for cg in connection_groups: - if cg["id"] != target_connection_group_id: - continue - metric_bounds = cg["connectivity-construct"][0]["service-slo-sle-policy"][ - "slo-policy" - ]["metric-bound"] - for metric in metric_bounds: - if metric["metric-type"] == "ietf-nss:one-way-delay-maximum": - constraint = Constraint() - constraint.sla_latency.e2e_latency_ms = float(metric["bound"]) - list_constraints.append(constraint) - elif metric["metric-type"] == "ietf-nss:one-way-bandwidth": - constraint = Constraint() - constraint.sla_capacity.capacity_gbps = ( - float(metric["bound"]) / 1.0e3 - ) - list_constraints.append(constraint) - break - else: - raise Exception("connection group not found") + sdps = slice_service["sdps"]["sdp"] + + # Find the referenced connection group + found_cg = next( + (cg for cg in connection_groups if cg["id"] == target_connection_group_id), + None, + ) + if not found_cg: + raise Exception( + f"Connection group '{target_connection_group_id}' not found" + ) + + # Build constraints from that connection group + list_constraints = build_constraints_from_connection_group(found_cg) + + # Add match-criterion to the relevant SDP + sdp_to_update = next((s for s in sdps if s["id"] == sdp_id), None) + if not sdp_to_update: + raise Exception(f"SDP '{sdp_id}' not found") + + sdp_to_update["service-match-criteria"]["match-criterion"].append( + new_match_criterion + ) + + # Update constraints at the slice level as needed del slice_request.slice_constraints[:] slice_request.slice_constraints.extend(list_constraints) - fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields - ) + slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED + + update_ietf_data_in_config(slice_request, CANDIDATE_RESOURCE_KEY, ietf_data) return slice_request @staticmethod @@ -507,60 +573,54 @@ class IETFSliceHandler: match_criterion_id: int, context_client: ContextClient, ) -> Slice: - # slice_request = get_slice_by_uuid(context_client, slice_uuid) + """ + Delete the specified match-criterion from an SDP in the slice's candidate IETF data. + """ slice_request = get_slice_by_defualt_name( context_client, slice_uuid, rw_copy=False ) - for cr in slice_request.slice_config.config_rules: - if cr.WhichOneof("config_rule") != "custom": - continue - if cr.custom.resource_key == CANDIDATE_RESOURCE_KEY: - ietf_data = json.loads(cr.custom.resource_value) - break - else: - raise Exception("ietf data not found") - slice_services = ietf_data["network-slice-services"]["slice-service"] - slice_service = slice_services[0] + ietf_data = get_ietf_data_from_config(slice_request, CANDIDATE_RESOURCE_KEY) + + slice_service = ietf_data["network-slice-services"]["slice-service"][0] sdps = slice_service["sdps"]["sdp"] - for sdp in sdps: - if sdp["id"] == sdp_id: - match_criteria = sdp["service-match-criteria"]["match-criterion"] - match_criterion_idx = [ - match_criterion["index"] == match_criterion_id - for match_criterion in match_criteria - ].index(True) - del match_criteria[match_criterion_idx] - break - else: - raise Exception("Second SDP not found") - fields = {name: (value, RAISE_IF_DIFFERS) for name, value in ietf_data.items()} - update_config_rule_custom( - slice_request.slice_config.config_rules, CANDIDATE_RESOURCE_KEY, fields + + # Find and modify the specified SDP + sdp_to_update = next((s for s in sdps if s["id"] == sdp_id), None) + if not sdp_to_update: + raise Exception(f"SDP '{sdp_id}' not found in slice '{slice_uuid}'") + + match_criteria = sdp_to_update["service-match-criteria"]["match-criterion"] + mc_index = next( + ( + i + for i, m in enumerate(match_criteria) + if m["index"] == match_criterion_id + ), + None, ) + if mc_index is None: + raise Exception( + f"No match-criterion with index '{match_criterion_id}' found in SDP '{sdp_id}'" + ) + + match_criteria.pop(mc_index) + update_ietf_data_in_config(slice_request, CANDIDATE_RESOURCE_KEY, ietf_data) return slice_request @staticmethod def copy_candidate_ietf_slice_data_to_running( slice_uuid: str, context_client: ContextClient ) -> Slice: - # slice_request = get_slice_by_uuid(context_client, slice_uuid) + """ + Copy candidate IETF slice data to the running IETF slice data for a given slice. + """ slice_request = get_slice_by_defualt_name( context_client, slice_uuid, rw_copy=False ) - for cr in slice_request.slice_config.config_rules: - if ( - cr.WhichOneof("config_rule") == "custom" - and cr.custom.resource_key == CANDIDATE_RESOURCE_KEY - ): - candidate_resource_value_dict = json.loads(cr.custom.resource_value) - fields = { - name: (value, RAISE_IF_DIFFERS) - for name, value in candidate_resource_value_dict.items() - } - break - else: - raise Exception("candidate ietf slice data not found") - update_config_rule_custom( - slice_request.slice_config.config_rules, RUNNING_RESOURCE_KEY, fields + candidate_ietf_data = get_ietf_data_from_config( + slice_request, CANDIDATE_RESOURCE_KEY + ) + update_ietf_data_in_config( + slice_request, RUNNING_RESOURCE_KEY, candidate_ietf_data ) return slice_request