From 7ee0b385b59f8041d1733710490e9899e9ac4f09 Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 2 Oct 2025 09:49:16 +0000 Subject: [PATCH 1/3] feat(e2e): Implement E2E orchestrator functionality - Added e2e_connect.py to handle E2E requests to the controller. - Created main.py to manage E2E realization logic based on intent and rules. - Introduced service_types for L3oWDM and L2VPN slices, including deletion capabilities. - Developed templates for Optical and IPoWDM services to standardize requests. - Enhanced main.py and select_way.py to support E2E controller type. - Updated send_controller.py to route requests to the E2E controller. - Added E2E namespace in Swagger for API documentation and interaction. - Modified build_response.py to accommodate E2E-specific response structures. --- app.py | 2 + src/config/config.py | 2 + src/main.py | 35 +-- src/mapper/main.py | 18 +- src/planner/energy_planner/energy.py | 275 ++++++++++++++++++ src/planner/hrat_planner/hrat.py | 52 ++++ src/planner/planner.py | 268 +---------------- .../tfs_optical_planner/tfs_optical.py | 254 ++++++++++++++++ src/realizer/e2e/e2e_connect.py | 49 ++++ src/realizer/e2e/main.py | 28 ++ .../e2e/service_types/del_l3ipowdm_slice.py | 177 +++++++++++ .../e2e/service_types/l3ipowdm_slice.py | 192 ++++++++++++ src/realizer/main.py | 37 ++- src/realizer/select_way.py | 7 +- src/realizer/send_controller.py | 5 +- src/templates/IPoWDM_orchestrator.json | 31 ++ src/templates/Optical_slice.json | 28 ++ src/templates/TAPI_service.json | 43 +++ src/utils/build_response.py | 97 +++--- swagger/E2E_namespace.py | 152 ++++++++++ 20 files changed, 1423 insertions(+), 329 deletions(-) create mode 100644 src/planner/energy_planner/energy.py create mode 100644 src/planner/hrat_planner/hrat.py create mode 100644 src/planner/tfs_optical_planner/tfs_optical.py create mode 100644 src/realizer/e2e/e2e_connect.py create mode 100644 src/realizer/e2e/main.py create mode 100644 src/realizer/e2e/service_types/del_l3ipowdm_slice.py create mode 100644 src/realizer/e2e/service_types/l3ipowdm_slice.py create mode 100644 src/templates/IPoWDM_orchestrator.json create mode 100644 src/templates/Optical_slice.json create mode 100644 src/templates/TAPI_service.json create mode 100644 swagger/E2E_namespace.py diff --git a/app.py b/app.py index ccdbed3..ceec6ae 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,7 @@ from flask_restx import Api from flask_cors import CORS from swagger.tfs_namespace import tfs_ns from swagger.ixia_namespace import ixia_ns +from swagger.E2E_namespace import e2e_ns from src.config.constants import NSC_PORT from src.webui.gui import gui_bp from src.config.config import create_config @@ -50,6 +51,7 @@ def create_app(): # Register namespaces api.add_namespace(tfs_ns, path="/tfs") api.add_namespace(ixia_ns, path="/ixia") + api.add_namespace(e2e_ns, path="/e2e") if app.config["WEBUI_DEPLOY"]: app.secret_key = "clave-secreta-dev" diff --git a/src/config/config.py b/src/config/config.py index 6d0f8d3..c7ac302 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -42,6 +42,7 @@ def create_config(app: Flask): # Mapper app.config["NRP_ENABLED"] = os.getenv("NRP_ENABLED", "false").lower() == "true" app.config["PLANNER_ENABLED"] = os.getenv("PLANNER_ENABLED", "false").lower() == "true" + app.config["PLANNER_TYPE"] = os.getenv("PLANNER_TYPE", "ENERGY") app.config["PCE_EXTERNAL"] = os.getenv("PCE_EXTERNAL", "false").lower() == "true" # Realizer @@ -51,6 +52,7 @@ def create_config(app: Flask): app.config["TFS_IP"] = os.getenv("TFS_IP", "127.0.0.1") app.config["UPLOAD_TYPE"] = os.getenv("UPLOAD_TYPE", "WEBUI") app.config["TFS_L2VPN_SUPPORT"] = os.getenv("TFS_L2VPN_SUPPORT", "false").lower() == "true" + app.config["TFS_E2E"] = os.getenv("TFS_E2E", "127.0.0.1") # IXIA app.config["IXIA_IP"] = os.getenv("IXIA_IP", "127.0.0.1") diff --git a/src/main.py b/src/main.py index 076c843..6a90dc5 100644 --- a/src/main.py +++ b/src/main.py @@ -14,6 +14,7 @@ # This file includes original contributions from Telefonica Innovación Digital S.L. +import logging import time from src.utils.dump_templates import dump_templates from src.utils.build_response import build_response @@ -25,11 +26,11 @@ from src.realizer.send_controller import send_controller class NSController: """ - Network Slice Controller (NSC) - A class to manage network slice creation, + Network Slice Controller (NSC) - A class to manage network slice creation, modification, and deletion across different network domains. - This controller handles the translation, mapping, and realization of network - slice intents from different formats (3GPP and IETF) to network-specific + This controller handles the translation, mapping, and realization of network + slice intents from different formats (3GPP and IETF) to network-specific configurations. Key Functionalities: @@ -39,14 +40,14 @@ class NSController: - Slice Realization: Convert intents to specific network configurations (L2VPN, L3VPN) """ - def __init__(self, controller_type = "TFS"): + def __init__(self, controller_type = "TFS"): """ Initialize the Network Slice Controller. Args: - controller_type (str): Flag to determine if configurations + controller_type (str): Flag to determine if configurations should be uploaded to Teraflow or IXIA system. - + Attributes: controller_type (str): Flag for Teraflow or Ixia upload answer (dict): Stores slice creation responses @@ -61,7 +62,7 @@ class NSController: self.start_time = 0 self.end_time = 0 self.setup_time = 0 - + def nsc(self, intent_json, slice_id=None): """ Main Network Slice Controller method to process and realize network slice intents. @@ -81,36 +82,36 @@ class NSController: Returns: tuple: Response status and HTTP status code - + """ # Start performance tracking self.start_time = time.perf_counter() # Reset requests requests = {"services":[]} - + # Process intent (translate if 3GPP) ietf_intents = nbi_processor(intent_json) - for intent in ietf_intents: + for intent in ietf_intents: # Mapper - mapper(intent) + rules = mapper(intent) # Store slice request details and build response - store_data(intent, slice_id, controller_type=self.controller_type) - self.response = build_response(intent, self.response) + store_data(intent, slice_id, controller_type=self.controller_type) + self.response = build_response(intent, self.response, controller_type= self.controller_type) # Realizer - request = realizer(intent, controller_type=self.controller_type, response = self.response) + request = realizer(intent, controller_type=self.controller_type, response = self.response, rules = rules) requests["services"].append(request) - + # Store the generated template for debugging dump_templates(intent_json, ietf_intents, requests) - + # Send config to controllers response = send_controller(self.controller_type, requests) if not response: raise Exception("Controller upload failed") - + # End performance tracking self.end_time = time.perf_counter() setup_time = (self.end_time - self.start_time) * 1000 diff --git a/src/mapper/main.py b/src/mapper/main.py index b738935..baeae47 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -14,7 +14,7 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import logging +import logging from src.planner.planner import Planner from .slo_viability import slo_viability from src.realizer.main import realizer @@ -36,7 +36,7 @@ def mapper(ietf_intent): Raises: Exception: If no suitable NRP is found and slice creation fails. - """ + """ if current_app.config["NRP_ENABLED"]: # Retrieve NRP view nrp_view = realizer(None, True, "READ") @@ -46,8 +46,8 @@ def mapper(ietf_intent): if slos: # Find candidate NRPs that can meet the SLO requirements candidates = [ - (nrp, slo_viability(slos, nrp)[1]) - for nrp in nrp_view + (nrp, slo_viability(slos, nrp)[1]) + for nrp in nrp_view if slo_viability(slos, nrp)[0] and nrp["available"] ] logging.debug(f"Candidates: {candidates}") @@ -61,13 +61,15 @@ def mapper(ietf_intent): # Update NRP view realizer(ietf_intent, True, "UPDATE") # TODO Here we should put how the slice is attached to an already created nrp - else: + else: # Request the controller to create a new NRP that meets the SLOs answer = realizer(ietf_intent, True, "CREATE", best_nrp) if not answer: - raise Exception("Slice rejected due to lack of NRPs") + raise Exception("Slice rejected due to lack of NRPs") # TODO Here we should put how the slice is attached to the new nrp if current_app.config["PLANNER_ENABLED"]: - optimal_path = Planner().planner(ietf_intent) - logging.debug(f"Optimal path: {optimal_path}") \ No newline at end of file + optimal_path = Planner().planner(ietf_intent, current_app.config["PLANNER_TYPE"]) + logging.debug(f"Optimal path: {optimal_path}") + return optimal_path + return None \ No newline at end of file diff --git a/src/planner/energy_planner/energy.py b/src/planner/energy_planner/energy.py new file mode 100644 index 0000000..aee53bc --- /dev/null +++ b/src/planner/energy_planner/energy.py @@ -0,0 +1,275 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + +import logging, random, os, json, heapq +from src.config.constants import SRC_PATH +from flask import current_app + + +def energy_planner(self, intent): + energy_metrics = self.__retrieve_energy() + topology = self.__retrieve_topology() + source = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[0].get("id") or "A" + destination = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[1].get("id") or "B" + optimal_path = [] + # If using an external PCE + if current_app.config["PCE_EXTERNAL"]: + logging.debug("Using external PCE for path planning") + def build_slice_input(node_source, node_destination): + return { + "clientName": "demo-client", + "requestId": random.randint(1000, 9999), + "sites": [node_source["nodeId"], node_destination["nodeId"]], + "graph": { + "nodes": [ + { + "nodeId": node_source["nodeId"], + "name": node_source["name"], + "footprint": node_source["footprint"], + "sticky": [node_source["nodeId"]] + }, + { + "nodeId": node_destination["nodeId"], + "name": node_destination["name"], + "footprint": node_destination["footprint"], + "sticky": [node_destination["nodeId"]] + } + ], + "links": [ + { + "fromNodeId": node_source["nodeId"], + "toNodeId": node_destination["nodeId"], + "bandwidth": 1000000000, + "metrics": [ + { + "metric": "DELAY", + "value": 10, + "bound": True, + "required": True + } + ] + } + ], + "constraints": { + "maxVulnerability": 3, + "maxDeployedServices": 10, + "metricLimits": [] + } + } + } + source = next((node for node in topology["nodes"] if node["name"] == source), None) + destination = next((node for node in topology["nodes"] if node["name"] == destination), None) + slice_input = build_slice_input(source, destination) + + # POST /sss/v1/slice/compute + def simulate_slice_output(input_data): + return { + "input": input_data, + "slice": { + "nodes": [ + {"site": 1, "service": 1}, + {"site": 2, "service": 2} + ], + "links": [ + { + "fromNodeId": 1, + "toNodeId": 2, + "lspId": 500, + "path": { + "ingressNodeId": 1, + "egressNodeId": 2, + "hops": [ + {"nodeId": 3, "linkId": "A-C", "portId": 1}, + {"nodeId": 2, "linkId": "C-B", "portId": 2} + ] + } + } + ], + "metric": {"value": 9} + }, + "error": None + } + slice_output = simulate_slice_output(slice_input) + # Mostrar resultado + optimal_path.append(source["name"]) + for link in slice_output["slice"]["links"]: + for hop in link["path"]["hops"]: + optimal_path.append(next((node for node in topology["nodes"] if node["nodeId"] == hop['nodeId']), None)["name"]) + + else: + logging.debug("Using internal PCE for path planning") + ietf_dlos = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] + logging.debug(ietf_dlos), + # Solo asigna los DLOS que existan, el resto a None + dlos = { + "EC": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_consumption"), None), + "CE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "carbon_emission"), None), + "EE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_efficiency"), None), + "URE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "renewable_energy_usage"), None) + } + logging.debug(f"Planning optimal path from {source} to {destination} with DLOS: {dlos}") + optimal_path = self.__calculate_optimal_path(topology, energy_metrics, source, destination, dlos) + + if not optimal_path: + logging.error("No valid path found") + raise Exception("No valid energy path found") + + return optimal_path + +def __retrieve_energy(self): + # TODO : Implement the logic to retrieve energy consumption data from controller + # Taking it from static file + with open(os.path.join(SRC_PATH, "planner/energy_ddbb.json"), "r") as archivo: + energy_metrics = json.load(archivo) + return energy_metrics + +def __retrieve_topology(self): + if current_app.config["PCE_EXTERNAL"]: + # TODO : Implement the logic to retrieve topology data from external PCE + # GET /sss/v1/topology/node and /sss/v1/topology/link + with open(os.path.join(SRC_PATH, "planner/ext_topo_ddbb.json"), "r") as archivo: + topology = json.load(archivo) + else: + # TODO : Implement the logic to retrieve topology data from controller + # Taking it from static file + with open(os.path.join(SRC_PATH, "planner/topo_ddbb.json"), "r") as archivo: + topology = json.load(archivo) + return topology + + + +def __calculate_optimal_path(self, topology, energy_metrics, source, destination, dlos): + logging.debug("Starting optimal path calculation...") + + # Create a dictionary with the weights of each node + node_data_map = {} + for node_data in energy_metrics: + node_id = node_data["name"] + ec = node_data["typical-power"] + ce = node_data["carbon-emissions"] + ee = node_data["efficiency"] + ure = node_data["renewable-energy-usage"] + + total_power_supply = sum(ps["typical-power"] for ps in node_data["power-supply"]) + total_power_boards = sum(b["typical-power"] for b in node_data["boards"]) + total_power_components = sum(c["typical-power"] for c in node_data["components"]) + total_power_transceivers = sum(t["typical-power"] for t in node_data["transceivers"]) + + logging.debug(f"Node {node_id}: EC={ec}, CE={ce}, EE={ee}, URE={ure}") + logging.debug(f"Node {node_id}: PS={total_power_supply}, BO={total_power_boards}, CO={total_power_components}, TR={total_power_transceivers}") + + weight = self.__compute_node_weight(ec, ce, ee, ure, + total_power_supply, + total_power_boards, + total_power_components, + total_power_transceivers) + logging.debug(f"Weight for node {node_id}: {weight}") + + node_data_map[node_id] = { + "weight": weight, + "ec": ec, + "ce": ce, + "ee": ee, + "ure": ure + } + + # Create a graph representation of the topology + graph = {} + for node in topology["ietf-network:networks"]["network"][0]["node"]: + graph[node["node-id"]] = [] + for link in topology["ietf-network:networks"]["network"][0]["link"]: + src = link["source"]["source-node"] + dst = link["destination"]["dest-node"] + graph[src].append((dst, node_data_map[dst]["weight"])) + logging.debug(f"Added link: {src} -> {dst} with weight {node_data_map[dst]['weight']}") + + # Dijkstra's algorithm with restrictions + queue = [(0, source, [], 0, 0, 0, 1)] # (accumulated cost, current node, path, sum_ec, sum_ce, sum_ee, min_ure) + visited = set() + + logging.debug(f"Starting search from {source} to {destination} with restrictions: {dlos}") + + + while queue: + cost, node, path, sum_ec, sum_ce, sum_ee, min_ure = heapq.heappop(queue) + logging.debug(f"Exploring node {node} with cost {cost} and path {path + [node]}") + + if node in visited: + logging.debug(f"Node {node} already visited, skipped.") + continue + visited.add(node) + path = path + [node] + + node_metrics = node_data_map[node] + sum_ec += node_metrics["ec"] + sum_ce += node_metrics["ce"] + sum_ee += node_metrics["ee"] + min_ure = min(min_ure, node_metrics["ure"]) if path[:-1] else node_metrics["ure"] + + logging.debug(f"Accumulated -> EC: {sum_ec}, CE: {sum_ce}, EE: {sum_ee}, URE min: {min_ure}") + + if dlos["EC"] is not None and sum_ec > dlos["EC"]: + logging.debug(f"Discarded path {path} for exceeding EC ({sum_ec} > {dlos['EC']})") + continue + if dlos["CE"] is not None and sum_ce > dlos["CE"]: + logging.debug(f"Discarded path {path} for exceeding CE ({sum_ce} > {dlos['CE']})") + continue + if dlos["EE"] is not None and sum_ee > dlos["EE"]: + logging.debug(f"Discarded path {path} for exceeding EE ({sum_ee} > {dlos['EE']})") + continue + if dlos["URE"] is not None and min_ure < dlos["URE"]: + logging.debug(f"Discarded path {path} for not reaching minimum URE ({min_ure} < {dlos['URE']})") + continue + + if node == destination: + logging.debug(f"Destination {destination} reached with a valid path: {path}") + return path + + for neighbor, weight in graph.get(node, []): + if neighbor not in visited: + logging.debug(f"Qeue -> neighbour: {neighbor}, weight: {weight}") + heapq.heappush(queue, ( + cost + weight, + neighbor, + path, + sum_ec, + sum_ce, + sum_ee, + min_ure + )) + logging.debug("No valid path found that meets the restrictions.") + return [] + + +def __compute_node_weight(self, ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, total_power_transceivers, alpha=1, beta=1, gamma=1, delta=1): + """ + Calcula el peso de un nodo con la fórmula: + w(v) = α·EC + β·CE + γ/EE + δ·(1 - URE) + """ + traffic = 100 + # Measure one hour of traffic + time = 1 + + power_idle = ec + total_power_supply + total_power_boards + total_power_components + total_power_transceivers + power_traffic = traffic * ee + + power_total = (power_idle + power_traffic) + + green_index = power_total * time / 1000 * (1 - ure) * ce + + return green_index + + diff --git a/src/planner/hrat_planner/hrat.py b/src/planner/hrat_planner/hrat.py new file mode 100644 index 0000000..c03560e --- /dev/null +++ b/src/planner/hrat_planner/hrat.py @@ -0,0 +1,52 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + +import logging +import requests + +def hrat_planner(data: str, action: str = "create") -> dict: + + data = {'network-slice-uuid': 'ecoc25-short-path-a7764e55-9bdb-4e38-9386-02ff47a33225', 'viability': True, 'actions': [{'type': 'CREATE_OPTICAL_SLICE', 'layer': 'OPTICAL', 'content': {'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd', 'service-interface-point': [{'uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a'}, {'uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625'}], 'node': [{'uuid': '68eb48ac-b686-5653-bdaf-7ccaeecd0709', 'owned-node-edge-point': [{'uuid': '7fd74b80-2b5a-55e2-8ef7-82bf589c9591', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '7b9f0b65-2387-5352-bc36-7173639463f0', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}, {'uuid': 'f55351ce-a5c8-50a7-b506-95b40e08bce4', 'owned-node-edge-point': [{'uuid': 'da6d924d-9cb4-5add-817d-f83e910beb2e', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '577ec899-ad92-5a19-a140-405a3cdbaa17', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}], 'link': [{'uuid': '3beef785-bb26-5741-af10-c5e1838c1701'}, {'uuid': '6144c664-246a-58ed-bf0a-7ec4286625da'}]}, 'controller-uuid': 'TAPI Optical Controller'}, {'type': 'PROVISION_MEDIA_CHANNEL_OLS_PATH', 'layer': 'OPTICAL', 'content': {'ols-path-uuid': 'cfeae4cb-c305-4884-9945-8b0c0f040c98', 'src-sip-uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a', 'dest-sip-uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625', 'direction': 'BIDIRECTIONAL', 'layer-protocol-name': 'PHOTONIC_MEDIA', 'layer-protocol-qualifier': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_MC', 'bandwidth-ghz': 100, 'link-uuid-path': ['3beef785-bb26-5741-af10-c5e1838c1701'], 'lower-frequency-mhz': '194700000', 'upper-frequency-mhz': '194800000', 'adjustment-granularity': 'G_6_25GHZ', 'grid-type': 'FLEX'}, 'controller-uuid': 'TAPI Optical Controller', 'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-1', 'termination-point-uuid': 'Ethernet110', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-2', 'termination-point-uuid': 'Ethernet220', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'CONFIG_VPNL3', 'layer': 'IP', 'content': {'tunnel-uuid': '9aae851a-eea9-4a28-969f-0e2c2196e936', 'src-node-uuid': 'Phoenix-1', 'src-ip-address': '10.10.1.1', 'src-ip-mask': '/24', 'src-vlan-id': 100, 'dest-node-uuid': 'Phoenix-2', 'dest-ip-address': '10.10.2.1', 'dest-ip-mask': '/24', 'dest-vlan-id': 100}, 'controller-uuid': 'IP Controller'}]} + return data + + # url = 'http://192.168.1.143:9090/api/resource-allocation/transport-network-slice-l3' + # headers = {'Content-Type': 'application/json'} + # try: + # if action == "delete": + # data = { + # "ietf-network-slice-service:network-slice-services": { + # "slice-service": [ + # { + # "id": data + # } + # ] + # } + # } + # response = requests.delete(url, headers=headers, json=data, timeout=15) + # elif action == "create": + # response = requests.post(url, headers=headers, json=data, timeout=15) + # else: + # raise ValueError("Invalid action. Use 'create' or 'delete'.") + # except requests.exceptions.RequestException as e: + # logging.error(f"HTTP request failed: {e}") + # return {} + + # # Check and return the response + # if response.ok: + # return response.json() + # else: + # print(f"Request failed with status code {response.status_code}: {response.text}") + # response.raise_for_status() diff --git a/src/planner/planner.py b/src/planner/planner.py index c2613bd..b44dc4c 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -14,269 +14,23 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import logging, random, os, json, heapq -from src.config.constants import SRC_PATH -from flask import current_app +import logging +from src.planner.energy_planner.energy import energy_planner +from src.planner.hrat_planner.hrat import hrat_planner +from src.planner.tfs_optical_planner.tfs_optical import tfs_optical_planner + class Planner: """ Planner class to compute the optimal path for a network slice based on energy consumption and topology. """ - def planner(self, intent): + def planner(self, intent, type): """ Plan the optimal path for a network slice based on energy consumption and topology. """ - energy_metrics = self.__retrieve_energy() - topology = self.__retrieve_topology() - source = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[0].get("id") or "A" - destination = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[1].get("id") or "B" - optimal_path = [] - # If using an external PCE - if current_app.config["PCE_EXTERNAL"]: - logging.debug("Using external PCE for path planning") - def build_slice_input(node_source, node_destination): - return { - "clientName": "demo-client", - "requestId": random.randint(1000, 9999), - "sites": [node_source["nodeId"], node_destination["nodeId"]], - "graph": { - "nodes": [ - { - "nodeId": node_source["nodeId"], - "name": node_source["name"], - "footprint": node_source["footprint"], - "sticky": [node_source["nodeId"]] - }, - { - "nodeId": node_destination["nodeId"], - "name": node_destination["name"], - "footprint": node_destination["footprint"], - "sticky": [node_destination["nodeId"]] - } - ], - "links": [ - { - "fromNodeId": node_source["nodeId"], - "toNodeId": node_destination["nodeId"], - "bandwidth": 1000000000, - "metrics": [ - { - "metric": "DELAY", - "value": 10, - "bound": True, - "required": True - } - ] - } - ], - "constraints": { - "maxVulnerability": 3, - "maxDeployedServices": 10, - "metricLimits": [] - } - } - } - source = next((node for node in topology["nodes"] if node["name"] == source), None) - destination = next((node for node in topology["nodes"] if node["name"] == destination), None) - slice_input = build_slice_input(source, destination) - - # POST /sss/v1/slice/compute - def simulate_slice_output(input_data): - return { - "input": input_data, - "slice": { - "nodes": [ - {"site": 1, "service": 1}, - {"site": 2, "service": 2} - ], - "links": [ - { - "fromNodeId": 1, - "toNodeId": 2, - "lspId": 500, - "path": { - "ingressNodeId": 1, - "egressNodeId": 2, - "hops": [ - {"nodeId": 3, "linkId": "A-C", "portId": 1}, - {"nodeId": 2, "linkId": "C-B", "portId": 2} - ] - } - } - ], - "metric": {"value": 9} - }, - "error": None - } - slice_output = simulate_slice_output(slice_input) - # Mostrar resultado - optimal_path.append(source["name"]) - for link in slice_output["slice"]["links"]: - for hop in link["path"]["hops"]: - optimal_path.append(next((node for node in topology["nodes"] if node["nodeId"] == hop['nodeId']), None)["name"]) - - else: - logging.debug("Using internal PCE for path planning") - ietf_dlos = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] - logging.debug(ietf_dlos), - # Solo asigna los DLOS que existan, el resto a None - dlos = { - "EC": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_consumption"), None), - "CE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "carbon_emission"), None), - "EE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_efficiency"), None), - "URE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "renewable_energy_usage"), None) - } - logging.debug(f"Planning optimal path from {source} to {destination} with DLOS: {dlos}") - optimal_path = self.__calculate_optimal_path(topology, energy_metrics, source, destination, dlos) - - if not optimal_path: - logging.error("No valid path found") - raise Exception("No valid energy path found") - - return optimal_path - - def __retrieve_energy(self): - # TODO : Implement the logic to retrieve energy consumption data from controller - # Taking it from static file - with open(os.path.join(SRC_PATH, "planner/energy_ddbb.json"), "r") as archivo: - energy_metrics = json.load(archivo) - return energy_metrics - - def __retrieve_topology(self): - if current_app.config["PCE_EXTERNAL"]: - # TODO : Implement the logic to retrieve topology data from external PCE - # GET /sss/v1/topology/node and /sss/v1/topology/link - with open(os.path.join(SRC_PATH, "planner/ext_topo_ddbb.json"), "r") as archivo: - topology = json.load(archivo) - else: - # TODO : Implement the logic to retrieve topology data from controller - # Taking it from static file - with open(os.path.join(SRC_PATH, "planner/topo_ddbb.json"), "r") as archivo: - topology = json.load(archivo) - return topology - - - - def __calculate_optimal_path(self, topology, energy_metrics, source, destination, dlos): - logging.debug("Starting optimal path calculation...") - - # Create a dictionary with the weights of each node - node_data_map = {} - for node_data in energy_metrics: - node_id = node_data["name"] - ec = node_data["typical-power"] - ce = node_data["carbon-emissions"] - ee = node_data["efficiency"] - ure = node_data["renewable-energy-usage"] - - total_power_supply = sum(ps["typical-power"] for ps in node_data["power-supply"]) - total_power_boards = sum(b["typical-power"] for b in node_data["boards"]) - total_power_components = sum(c["typical-power"] for c in node_data["components"]) - total_power_transceivers = sum(t["typical-power"] for t in node_data["transceivers"]) - - logging.debug(f"Node {node_id}: EC={ec}, CE={ce}, EE={ee}, URE={ure}") - logging.debug(f"Node {node_id}: PS={total_power_supply}, BO={total_power_boards}, CO={total_power_components}, TR={total_power_transceivers}") - - weight = self.__compute_node_weight(ec, ce, ee, ure, - total_power_supply, - total_power_boards, - total_power_components, - total_power_transceivers) - logging.debug(f"Weight for node {node_id}: {weight}") - - node_data_map[node_id] = { - "weight": weight, - "ec": ec, - "ce": ce, - "ee": ee, - "ure": ure - } - - # Create a graph representation of the topology - graph = {} - for node in topology["ietf-network:networks"]["network"][0]["node"]: - graph[node["node-id"]] = [] - for link in topology["ietf-network:networks"]["network"][0]["link"]: - src = link["source"]["source-node"] - dst = link["destination"]["dest-node"] - graph[src].append((dst, node_data_map[dst]["weight"])) - logging.debug(f"Added link: {src} -> {dst} with weight {node_data_map[dst]['weight']}") - - # Dijkstra's algorithm with restrictions - queue = [(0, source, [], 0, 0, 0, 1)] # (accumulated cost, current node, path, sum_ec, sum_ce, sum_ee, min_ure) - visited = set() - - logging.debug(f"Starting search from {source} to {destination} with restrictions: {dlos}") - - - while queue: - cost, node, path, sum_ec, sum_ce, sum_ee, min_ure = heapq.heappop(queue) - logging.debug(f"Exploring node {node} with cost {cost} and path {path + [node]}") - - if node in visited: - logging.debug(f"Node {node} already visited, skipped.") - continue - visited.add(node) - path = path + [node] - - node_metrics = node_data_map[node] - sum_ec += node_metrics["ec"] - sum_ce += node_metrics["ce"] - sum_ee += node_metrics["ee"] - min_ure = min(min_ure, node_metrics["ure"]) if path[:-1] else node_metrics["ure"] - - logging.debug(f"Accumulated -> EC: {sum_ec}, CE: {sum_ce}, EE: {sum_ee}, URE min: {min_ure}") - - if dlos["EC"] is not None and sum_ec > dlos["EC"]: - logging.debug(f"Discarded path {path} for exceeding EC ({sum_ec} > {dlos['EC']})") - continue - if dlos["CE"] is not None and sum_ce > dlos["CE"]: - logging.debug(f"Discarded path {path} for exceeding CE ({sum_ce} > {dlos['CE']})") - continue - if dlos["EE"] is not None and sum_ee > dlos["EE"]: - logging.debug(f"Discarded path {path} for exceeding EE ({sum_ee} > {dlos['EE']})") - continue - if dlos["URE"] is not None and min_ure < dlos["URE"]: - logging.debug(f"Discarded path {path} for not reaching minimum URE ({min_ure} < {dlos['URE']})") - continue - - if node == destination: - logging.debug(f"Destination {destination} reached with a valid path: {path}") - return path - - for neighbor, weight in graph.get(node, []): - if neighbor not in visited: - logging.debug(f"Qeue -> neighbour: {neighbor}, weight: {weight}") - heapq.heappush(queue, ( - cost + weight, - neighbor, - path, - sum_ec, - sum_ce, - sum_ee, - min_ure - )) - logging.debug("No valid path found that meets the restrictions.") - return [] - - - def __compute_node_weight(self, ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, total_power_transceivers, alpha=1, beta=1, gamma=1, delta=1): - """ - Calcula el peso de un nodo con la fórmula: - w(v) = α·EC + β·CE + γ/EE + δ·(1 - URE) - """ - traffic = 100 - # Measure one hour of traffic - time = 1 - - power_idle = ec + total_power_supply + total_power_boards + total_power_components + total_power_transceivers - power_traffic = traffic * ee - - power_total = (power_idle + power_traffic) - - green_index = power_total * time / 1000 * (1 - ure) * ce - - return green_index - - + logging.info(f"Planner type selected: {type}") + if type == "ENERGY" : return energy_planner(intent) + elif type == "HRAT" : return hrat_planner(intent) + elif type == "TFS_OPTICAL": return tfs_optical_planner(intent, action = "create") + else : return None diff --git a/src/planner/tfs_optical_planner/tfs_optical.py b/src/planner/tfs_optical_planner/tfs_optical.py new file mode 100644 index 0000000..cebc61b --- /dev/null +++ b/src/planner/tfs_optical_planner/tfs_optical.py @@ -0,0 +1,254 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. +import logging +import requests +import os +import uuid +import json +from src.config.constants import TEMPLATES_PATH + +def tfs_optical_planner(intent, action: str = "create") -> dict: + if action == 'delete': + logging.info("DELETE REQUEST RECEIVED: %s", intent) + with open(os.path.join(TEMPLATES_PATH, "slice.db"), 'r', encoding='utf-8') as file: + slices = json.load(file) + + for slice_obj in slices: + if 'slice_id' in slice_obj and slice_obj['slice_id'] == intent: + logging.info("Slice found: %s", slice_obj['slice_id']) + source = None + destination = None + services = slice_obj['intent']['ietf-network-slice-service:network-slice-services']['slice-service'] + for service in services: + c_groups = service.get("connection-groups", {}).get("connection-group", []) + for cg in c_groups: + constructs = cg.get("connectivity-construct", []) + for construct in constructs: + if "p2mp-sdp" in construct: + source = construct["p2mp-sdp"]["root-sdp-id"] + destination = construct["p2mp-sdp"]["leaf-sdp-id"] + break + if source and destination: + break + response = send_request(source, destination) + summary = { + "source": source, + "destination": destination, + "connectivity-service": response + } + rules = generate_rules(summary, intent, action) + else: + services = intent["ietf-network-slice-service:network-slice-services"]["slice-service"] + source = None + destination = None + for service in services: + c_groups = service.get("connection-groups", {}).get("connection-group", []) + for cg in c_groups: + constructs = cg.get("connectivity-construct", []) + for construct in constructs: + if "p2mp-sdp" in construct: + source = construct["p2mp-sdp"]["root-sdp-id"] + destination = construct["p2mp-sdp"]["leaf-sdp-id"] + break + if source and destination: + break + + response = send_request(source, destination) + + summary = { + "source": source, + "destination": destination, + "connectivity-service": response + } + logging.info(summary) + rules = generate_rules(summary, intent,action) + return rules + +def send_request(source, destination): + url = "http://10.30.7.66:31060/OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp" + + headers = { + "Content-Type": "application/json", + "Accept": "*/*" + } + + if isinstance(source, str): + sources_list = [source] + else: + sources_list = list(source) + + if isinstance(destination, str): + destinations_list = [destination] + else: + destinations_list = list(destination) + + payload = { + "sources": sources_list, + "destinations": destinations_list, + "bitrate": 100, + "bidirectional": True, + "band": 200, + "subcarriers_per_source": [4] * len(sources_list) + } + logging.info(f"Payload for path computation: {json.dumps(payload, indent=2)}") + + response = requests.post(url, headers=headers, data=json.dumps(payload)) + return json.loads(response.text) + +def group_block(group, action, group_id_override=None, node = None): + active = "true" if action == 'create' else "false" + group_id = group_id_override if group_id_override is not None else group["digital_sub_carriers_group_id"] + if node == "leaf": + return { + "digital_sub_carriers_group_id": group_id, + "digital_sub_carrier_id": [ + {'sub_carrier_id': 1, 'active': active}, + {'sub_carrier_id': 2, 'active': active}, + {'sub_carrier_id': 3, 'active': active}, + {'sub_carrier_id': 4, 'active': active} + ] + } + else: + return { + "digital_sub_carriers_group_id": group_id, + "digital_sub_carrier_id": [ + { + "sub_carrier_id": sid, + "active": active, + } + for sid in group["subcarrier-id"] + ] + } + +def generate_rules(connectivity_service, intent, action): + src_name = connectivity_service.get("source", "FALTA VALOR") + dest_list = connectivity_service.get("destination", ["FALTA VALOR"]) + dest_str = ",".join(dest_list) + config_rules = [] + + network_slice_uuid_str = f"{src_name}_to_{dest_str}" + tunnel_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, network_slice_uuid_str)) + provisionamiento = { + "network-slice-uuid": network_slice_uuid_str, + "viability": True, + "actions": [] + } + + attributes = connectivity_service["connectivity-service"]["tapi-connectivity:connectivity-service"]["connection"][0]["optical-connection-attributes"] + groups = attributes["subcarrier-attributes"]["digital-subcarrier-group"] + operational_mode = attributes["modulation"]["operational-mode"] + hub_groups = [ + group_block(group, action, group_id_override=index + 1) + for index, group in enumerate(groups) + ] + hub = { + "name": "channel-1", + "frequency": 195000000, + "target_output_power": 0, + "operational_mode": operational_mode, + "operation" : "merge", + "digital_sub_carriers_group": hub_groups + } + + leaves = [] + for dest, group in zip(connectivity_service["destination"], groups): + if dest == "T1.1": + name = "channel-1" + freq = 195006250 + if dest == "T1.2": + name = "channel-3" + freq = 195018750 + if dest == "T1.3": + name = "channel-5" + freq = 195031250 + leaf = { + "name": name, + "frequency": freq, + "target_output_power": group["Tx-power"], + "operational_mode": int(group["operational-mode"]), + "operation" : "merge", + "digital_sub_carriers_group": [group_block(group, action, group_id_override=1, node = "leaf")] + } + + leaves.append(leaf) + + final_json = {"components": [hub] + leaves} + if action == 'create': + provisionamiento["actions"].append({ + "type": "XR_AGENT_ACTIVATE_TRANSCEIVER", + "layer": "OPTICAL", + "content": final_json, + "controller-uuid": "IPoWDM Controller" + }) + + nodes = {} + sdp_list = intent['ietf-network-slice-service:network-slice-services']['slice-service'][0]['sdps']['sdp'] + + for sdp in sdp_list: + node = sdp['node-id'] + attachments = sdp['attachment-circuits']['attachment-circuit'] + for ac in attachments: + ip = ac.get('ac-ipv4-address', None) + prefix = ac.get('ac-ipv4-prefix-length', None) + vlan = 500 + nodes[node] = { + "ip-address": ip, + "ip-mask": prefix, + "vlan-id": vlan + } + + provisionamiento["actions"].append({ + "type": "CONFIG_VPNL3", + "layer": "IP", + "content": { + "tunnel-uuid": tunnel_uuid, + "src-node-uuid": src_name, + "src-ip-address": nodes[src_name]["ip-address"], + "src-ip-mask": str(nodes[src_name]["ip-mask"]), + "src-vlan-id": nodes[src_name]["vlan-id"], + "dest1-node-uuid": dest_list[0], + "dest1-ip-address": nodes[dest_list[0]]["ip-address"], + "dest1-ip-mask": str(nodes[dest_list[0]]["ip-mask"]), + "dest1-vlan-id": nodes[dest_list[0]]["vlan-id"], + "dest2-node-uuid": dest_list[1], + "dest2-ip-address": nodes[dest_list[1]]["ip-address"], + "dest2-ip-mask": str(nodes[dest_list[1]]["ip-mask"]), + "dest2-vlan-id": nodes[dest_list[1]]["vlan-id"], + "dest3-node-uuid": dest_list[2], + "dest3-ip-address": nodes[dest_list[2]]["ip-address"], + "dest3-ip-mask": str(nodes[dest_list[2]]["ip-mask"]), + "dest3-vlan-id": nodes[dest_list[2]]["vlan-id"] + }, + "controller-uuid": "IP Controller" + }) + + config_rules.append(provisionamiento) + else: + nodes = [] + nodes.append(src_name) + for dst in dest_list: nodes.append(dst) + aux = tunnel_uuid + '-' + src_name + '-' + '-'.join(dest_list) + provisionamiento["actions"].append({ + "type": "DEACTIVATE_XR_AGENT_TRANSCEIVER", + "layer": "OPTICAL", + "content": final_json, + "controller-uuid": "IPoWDM Controller", + "uuid" : aux, + "nodes": nodes + }) + config_rules.append(provisionamiento) + + return config_rules diff --git a/src/realizer/e2e/e2e_connect.py b/src/realizer/e2e/e2e_connect.py new file mode 100644 index 0000000..ad100d3 --- /dev/null +++ b/src/realizer/e2e/e2e_connect.py @@ -0,0 +1,49 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + +import json +import logging +from flask import current_app +from src.utils.send_response import send_response + +def e2e_connect(requests, controller_ip): + for request in requests["services"]: + for service in request: + logging.info(f"DATOS A ENVIAR: {service}") + # user="admin" + # password="admin" + # token="" + # session = requests.Session() + # session.auth = (user, password) + # url=f'http://{controller_ip}/webui' + # response=session.get(url=url) + # for item in response.iter_lines(): + # if"csrf_token" in str(item): + # string=str(item).split(' 0: rules = rules[0] + actions = rules.get("actions", []) + + has_transceiver = any(a.get("type", "").startswith("XR_AGENT_ACTIVATE_TRANSCEIVER") for a in actions) + has_optical = any(a.get("type", "").startswith("PROVISION_MEDIA_CHANNEL") for a in actions) + has_l3 = any(a.get("type", "").startswith("CONFIG_VPNL3") for a in actions) + has_l2 = any(a.get("type", "").startswith("CONFIG_VPNL2") for a in actions) + + del_transceiver = any(a.get("type", "").startswith("DEACTIVATE_XR_AGENT_TRANSCEIVER") for a in actions) + del_optical = any(a.get("type", "").startswith("DEPROVISION_OPTICAL_RESOURCE") for a in actions) + del_l3 = any(a.get("type", "").startswith("REMOVE_VPNL3") for a in actions) + del_l2 = any(a.get("type", "").startswith("REMOVE_VPNL2") for a in actions) + + if has_transceiver: selected_way = "L3oWDM" + elif has_optical and has_l3: selected_way = "L3oWDM" + elif has_optical and has_l2: selected_way = "L2oWDM" + elif has_optical: selected_way = "OPTIC" + elif has_l3: selected_way = "L3VPN" + elif has_l2: selected_way = "L2VPN" + + elif del_transceiver: selected_way = "DEL_L3oWDM" + elif del_optical and del_l3: selected_way = "DEL_L3oWDM" + elif del_optical and del_l2: selected_way = "DEL_L2oWDM" + elif del_optical: selected_way = "DEL_OPTIC" + elif del_l3: selected_way = "DEL_L3VPN" + elif del_l2: selected_way = "DEL_L2VPN" + else: raise ValueError("Cannot determine the realization way from rules") + way = selected_way + else: + way = safe_get(ietf_intent, ['ietf-network-slice-service:network-slice-services', 'slice-service', 0, 'service-tags', 'tag-type', 0, 'tag-type-value', 0]) + logging.info(f"Selected way: {way}") + request = select_way(controller=controller_type, way=way, ietf_intent=ietf_intent, response=response, rules = rules) return request diff --git a/src/realizer/select_way.py b/src/realizer/select_way.py index 110b18a..2b9d2da 100644 --- a/src/realizer/select_way.py +++ b/src/realizer/select_way.py @@ -16,9 +16,10 @@ import logging from .ixia.main import ixia -from .tfs.main import tfs +from .tfs.main import tfs +from .e2e.main import e2e -def select_way(controller=None, way=None, ietf_intent=None, response=None): +def select_way(controller=None, way=None, ietf_intent=None, response=None, rules = None): """ Determine the method of slice realization. @@ -43,6 +44,8 @@ def select_way(controller=None, way=None, ietf_intent=None, response=None): realizing_request = tfs(ietf_intent, way, response) elif controller == "IXIA": realizing_request = ixia(ietf_intent) + elif controller == "E2E": + realizing_request = e2e(ietf_intent, way, response, rules) else: logging.warning(f"Unsupported controller: {controller}. Defaulting to TFS realization.") realizing_request = tfs(ietf_intent, way, response) diff --git a/src/realizer/send_controller.py b/src/realizer/send_controller.py index 9bb81af..a59e7bb 100644 --- a/src/realizer/send_controller.py +++ b/src/realizer/send_controller.py @@ -18,9 +18,10 @@ import logging from flask import current_app from .tfs.tfs_connect import tfs_connect from .ixia.ixia_connect import ixia_connect +from .e2e.e2e_connect import e2e_connect def send_controller(controller_type, requests): - if current_app.config["DUMMY_MODE"]: + if current_app.config["DUMMY_MODE"]: return True if controller_type == "TFS": response = tfs_connect(requests, current_app.config["TFS_IP"]) @@ -28,4 +29,6 @@ def send_controller(controller_type, requests): elif controller_type == "IXIA": response = ixia_connect(requests, current_app.config["IXIA_IP"]) logging.info("Requests sent to Ixia") + elif controller_type == "E2E": + response = e2e_connect(requests, current_app.config["TFS_E2E"]) return response diff --git a/src/templates/IPoWDM_orchestrator.json b/src/templates/IPoWDM_orchestrator.json new file mode 100644 index 0000000..60eb0db --- /dev/null +++ b/src/templates/IPoWDM_orchestrator.json @@ -0,0 +1,31 @@ +{ + "services": [ + { + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "TAPI LSP"} + }, + "service_type": 12, + "service_status": {"service_status": 1}, + "service_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "TFS-OPTICAL"}},"endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "TFS-PACKET"}},"endpoint_uuid": {"uuid": "mgmt"}} + + ], + "service_constraints": [], + + "service_config": {"config_rules": [ + {"action": 1, "ipowdm": { + "endpoint_id": { + "device_id": {"device_uuid": {"uuid": "TFS-PACKET"}}, + "endpoint_uuid": {"uuid": "mgmt"} + }, + "rule_set": { + "src" : [], + "dst" : [] + } + }} + ]} + } + ] +} \ No newline at end of file diff --git a/src/templates/Optical_slice.json b/src/templates/Optical_slice.json new file mode 100644 index 0000000..94c87fe --- /dev/null +++ b/src/templates/Optical_slice.json @@ -0,0 +1,28 @@ +{ + "tapi-common:context" : { + "name" : [ + { + "value" : "" + } + ], + "service-interface-point" : [ + { + "uuid" : "" + }, + { + "uuid" : "" + } + ], + "tapi-topology:topology-context" : { + "topology" : [ + { + "link" : [ + ], + "node" : [ + ] + } + ] + }, + "uuid" : "" + } +} diff --git a/src/templates/TAPI_service.json b/src/templates/TAPI_service.json new file mode 100644 index 0000000..1b09a04 --- /dev/null +++ b/src/templates/TAPI_service.json @@ -0,0 +1,43 @@ +{ + "services": [ + { + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "TAPI LSP"} + }, + "service_type": 11, + "service_status": {"service_status": 1}, + "service_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "TFS-OPTICAL"}},"endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "TFS-PACKET"}},"endpoint_uuid": {"uuid": "mgmt"}} + + ], + "service_constraints": [], + + "service_config": {"config_rules": [ + {"action": 1, "tapi_lsp": { + "endpoint_id": { + "device_id": {"device_uuid": {"uuid": "TFS-OPTICAL"}}, + "endpoint_uuid": {"uuid": "mgmt"} + }, + "rule_set": { + "src": "", + "dst": "", + "uuid": "", + "bw": "", + "tenant_uuid": "", + "direction": "", + "layer_protocol_name": "", + "layer_protocol_qualifier": "", + "lower_frequency_mhz": "", + "upper_frequency_mhz": "", + "link_uuid_path": [ + ], + "granularity": "", + "grid_type": "" + } + }} + ]} + } + ] +} \ No newline at end of file diff --git a/src/utils/build_response.py b/src/utils/build_response.py index c013602..0f96cfa 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -14,48 +14,63 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -def build_response(intent, response): - - id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] - source = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] - destination = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] - vlan = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] - - # Extract QoS Profile from intent - #QoSProfile = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] - - qos_requirements = [] - - # Populate response with QoS requirements and VLAN from intent - slo_policy = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"] - - # Process metrics - for metric in slo_policy.get("metric-bound", []): - constraint_type = f"{metric['metric-type']}[{metric['metric-unit']}]" - constraint_value = str(metric["bound"]) - qos_requirements.append({ - "constraint_type": constraint_type, - "constraint_value": constraint_value - }) +def build_response(intent, response, controller_type = None): - # Availability - if "availability" in slo_policy: - qos_requirements.append({ - "constraint_type": "availability[%]", - "constraint_value": str(slo_policy["availability"]) - }) + if controller_type == "E2E": + id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] + source = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["id"] + destination = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["id"] + vlan = None + qos_requirements = [] + + response.append({ + "id": id, + "source": source, + "destination": destination, + "vlan": vlan, + "requirements": qos_requirements, + }) + else: + id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] + source = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] + destination = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] + vlan = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] + + # Extract QoS Profile from intent + #QoSProfile = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] + + qos_requirements = [] + + # Populate response with QoS requirements and VLAN from intent + slo_policy = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"] + + # Process metrics + for metric in slo_policy.get("metric-bound", []): + constraint_type = f"{metric['metric-type']}[{metric['metric-unit']}]" + constraint_value = str(metric["bound"]) + qos_requirements.append({ + "constraint_type": constraint_type, + "constraint_value": constraint_value + }) + + # Availability + if "availability" in slo_policy: + qos_requirements.append({ + "constraint_type": "availability[%]", + "constraint_value": str(slo_policy["availability"]) + }) - # MTU - if "mtu" in slo_policy: - qos_requirements.append({ - "constraint_type": "mtu[bytes]", - "constraint_value": str(slo_policy["mtu"]) + # MTU + if "mtu" in slo_policy: + qos_requirements.append({ + "constraint_type": "mtu[bytes]", + "constraint_value": str(slo_policy["mtu"]) + }) + response.append({ + "id": id, + "source": source, + "destination": destination, + "vlan": vlan, + "requirements": qos_requirements, }) - response.append({ - "id": id, - "source": source, - "destination": destination, - "vlan": vlan, - "requirements": qos_requirements, - }) return response \ No newline at end of file diff --git a/swagger/E2E_namespace.py b/swagger/E2E_namespace.py new file mode 100644 index 0000000..86c9915 --- /dev/null +++ b/swagger/E2E_namespace.py @@ -0,0 +1,152 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (E2E) (https://E2E.etsi.org/) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + +from flask import request +from flask_restx import Namespace, Resource, reqparse +from src.main import NSController +from src.api.main import Api +import json +from swagger.models.create_models import create_gpp_nrm_28541_model, create_ietf_network_slice_nbi_yang_model + +e2e_ns = Namespace( + "E2E", + description="Operations related to transport network slices with E2E Orchestrator" +) + + +# 3GPP NRM TS28.541 Data models +gpp_network_slice_request_model = create_gpp_nrm_28541_model(e2e_ns) + +# IETF draft-ietf-teas-ietf-network-slice-nbi-yang Data models + +slice_ddbb_model, slice_response_model = create_ietf_network_slice_nbi_yang_model(e2e_ns) + +upload_parser = reqparse.RequestParser() +upload_parser.add_argument('file', location='files', type='FileStorage', help="File to upload") +upload_parser.add_argument('json_data', location='form', help="JSON Data in string format") + +# Namespace Controllers +@e2e_ns.route("/slice") +class E2ESliceList(Resource): + @e2e_ns.doc(summary="Return all transport network slices", description="Returns all transport network slices from the slice controller.") + @e2e_ns.response(200, "Slices returned", slice_ddbb_model) + @e2e_ns.response(404, "Transport network slices not found") + @e2e_ns.response(500, "Internal server error") + def get(self): + """Retrieve all slices""" + controller = NSController(controller_type="E2E") + data, code = Api(controller).get_flows() + return data, code + + @e2e_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") + @e2e_ns.response(201,"Slice created successfully", slice_response_model) + @e2e_ns.response(400, "Invalid request format") + @e2e_ns.response(500, "Internal server error") + @e2e_ns.expect(upload_parser) + def post(self): + """Submit a new slice request with a file""" + + json_data = None + + # Try to get the JSON data from the uploaded file + uploaded_file = request.files.get('file') + if uploaded_file: + if not uploaded_file.filename.endswith('.json'): + return { + "success": False, + "data": None, + "error": "Only JSON files allowed" + }, 400 + + try: + json_data = json.load(uploaded_file) # Convert file to JSON + except json.JSONDecodeError: + return { + "success": False, + "data": None, + "error": "JSON file not valid" + }, 400 + + # If no file was uploaded, try to get the JSON data from the form + if json_data is None: + raw_json = request.form.get('json_data') + if raw_json: + try: + json_data = json.loads(raw_json) # Convert string to JSON + except json.JSONDecodeError: + return { + "success": False, + "data": None, + "error": "JSON file not valid" + }, 400 + + # If no JSON data was found, return an error + if json_data is None: + return { + "success": False, + "data": None, + "error": "No data sent" + }, 400 + + # Process the JSON data with the NSController + controller = NSController(controller_type="E2E") + data, code = Api(controller).add_flow(json_data) + return data, code + + @e2e_ns.doc(summary="Delete all transport network slices", description="Deletes all transport network slices from the slice controller.") + @e2e_ns.response(204, "All transport network slices deleted successfully.") + @e2e_ns.response(500, "Internal server error") + def delete(self): + """Delete all slices""" + controller = NSController(controller_type="E2E") + data, code = Api(controller).delete_flows() + return data, code + + +@e2e_ns.route("/slice/") +@e2e_ns.doc(params={"slice_id": "The ID of the slice to retrieve or modify"}) +class E2ESlice(Resource): + @e2e_ns.doc(summary="Return a specific transport network slice", description="Returns specific information related to a slice by providing its id") + @e2e_ns.response(200, "Slice returned", slice_ddbb_model) + @e2e_ns.response(404, "Transport network slice not found.") + @e2e_ns.response(500, "Internal server error") + def get(self, slice_id): + """Retrieve a specific slice""" + controller = NSController(controller_type="E2E") + data, code = Api(controller).get_flows(slice_id) + return data, code + + @e2e_ns.doc(summary="Delete a specific transport network slice", description="Deletes a specific transport network slice from the slice controller based on the provided `slice_id`.") + @e2e_ns.response(204, "Transport network slice deleted successfully.") + @e2e_ns.response(404, "Transport network slice not found.") + @e2e_ns.response(500, "Internal server error") + def delete(self, slice_id): + """Delete a slice""" + controller = NSController(controller_type="E2E") + data, code = Api(controller).delete_flows(slice_id) + return data, code + + @e2e_ns.expect(slice_ddbb_model, validate=True) + @e2e_ns.doc(summary="Modify a specific transport network slice", description="Returns a specific slice that has been modified") + @e2e_ns.response(200, "Slice modified", slice_response_model) + @e2e_ns.response(404, "Transport network slice not found.") + @e2e_ns.response(500, "Internal server error") + def put(self, slice_id): + """Modify a slice""" + json_data = request.get_json() + controller = NSController(controller_type="E2E") + data, code = Api(controller).modify_flow(slice_id, json_data) + return data, code -- GitLab From 9e160aa9b92316fcabe890c967722c152c617bf0 Mon Sep 17 00:00:00 2001 From: velazquez Date: Tue, 14 Oct 2025 12:45:20 +0200 Subject: [PATCH 2/3] General Slices are stored only when the request is processed, to avoid cases where slice is stored but the request is not fully processed Add new error (RuntimeError) to handle cases were the request is processed but no service is created Change some error raised to be consistent in the whole app Create flags to select the IP of HRAT and optical planner IPs Update env.example with new flags Change build_response to be the same for every request Planner Both HRAT and optical planner are configured through ip in the config Add exception handling in HRAT and optical planner to avoid errors Add "allowed_ids" variable in energy planner to handle only requests involving specific ids, to avoid errors in energy planning Realizer Change e2e realizer to handle cases with no rules avoiding errors Change e2e_connect to use the tfs_connector class --- src/api/main.py | 3 + src/config/.env.example | 11 +++ src/config/config.py | 6 +- src/main.py | 13 ++- src/mapper/main.py | 3 +- src/planner/energy_planner/energy.py | 38 ++++---- .../{ => energy_planner}/energy_ddbb.json | 0 .../{ => energy_planner}/ext_topo_ddbb.json | 0 .../{ => energy_planner}/topo_ddbb.json | 0 src/planner/hrat_planner/hrat.py | 75 ++++++++------- src/planner/planner.py | 5 +- .../tfs_optical_planner/tfs_optical.py | 52 +++++----- src/realizer/e2e/e2e_connect.py | 34 +------ src/realizer/e2e/main.py | 2 +- .../e2e/service_types/l3ipowdm_slice.py | 14 +-- src/realizer/main.py | 6 +- src/realizer/send_controller.py | 1 + src/realizer/tfs/service_types/tfs_l2vpn.py | 13 ++- src/realizer/tfs/service_types/tfs_l3vpn.py | 13 ++- src/utils/build_response.py | 94 ++++++++----------- swagger/E2E_namespace.py | 1 + swagger/ixia_namespace.py | 1 + swagger/tfs_namespace.py | 1 + 23 files changed, 204 insertions(+), 182 deletions(-) rename src/planner/{ => energy_planner}/energy_ddbb.json (100%) rename src/planner/{ => energy_planner}/ext_topo_ddbb.json (100%) rename src/planner/{ => energy_planner}/topo_ddbb.json (100%) diff --git a/src/api/main.py b/src/api/main.py index 344171c..441bed0 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -52,6 +52,9 @@ class Api: code=201, data=result ) + except RuntimeError as e: + # Handle case where there is no content to process + return send_response(False, code=200, message=str(e)) except Exception as e: # Handle unexpected errors return send_response(False, code=500, message=str(e)) diff --git a/src/config/.env.example b/src/config/.env.example index 4037615..784d74e 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -29,6 +29,12 @@ NRP_ENABLED=false PLANNER_ENABLED=true # Flag to determine if external PCE is used PCE_EXTERNAL=false +# Type of planner to be used. Options: ENERGY, HRAT, TFS_OPTICAL +PLANNER_TYPE=ENERGY +# HRAT +HRAT_IP=10.0.0.1 +# TFS_OPTICAL +OPTICAL_PLANNER_IP=10.0.0.1 # ------------------------- # Realizer @@ -49,6 +55,11 @@ TFS_L2VPN_SUPPORT=false # ------------------------- IXIA_IP=127.0.0.1 +# ------------------------- +# E2E Controller +# ------------------------- +TFS_E2E_IP=127.0.0.1 + # ------------------------- # WebUI # ------------------------- diff --git a/src/config/config.py b/src/config/config.py index c7ac302..04b72aa 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -44,6 +44,8 @@ def create_config(app: Flask): app.config["PLANNER_ENABLED"] = os.getenv("PLANNER_ENABLED", "false").lower() == "true" app.config["PLANNER_TYPE"] = os.getenv("PLANNER_TYPE", "ENERGY") app.config["PCE_EXTERNAL"] = os.getenv("PCE_EXTERNAL", "false").lower() == "true" + app.config["HRAT_IP"] = os.getenv("HRAT_IP", "192.168.1.143") + app.config["OPTICAL_PLANNER_IP"] = os.getenv("OPTICAL_PLANNER_IP", "10.30.7.66") # Realizer app.config["DUMMY_MODE"] = os.getenv("DUMMY_MODE", "true").lower() == "true" @@ -52,11 +54,13 @@ def create_config(app: Flask): app.config["TFS_IP"] = os.getenv("TFS_IP", "127.0.0.1") app.config["UPLOAD_TYPE"] = os.getenv("UPLOAD_TYPE", "WEBUI") app.config["TFS_L2VPN_SUPPORT"] = os.getenv("TFS_L2VPN_SUPPORT", "false").lower() == "true" - app.config["TFS_E2E"] = os.getenv("TFS_E2E", "127.0.0.1") # IXIA app.config["IXIA_IP"] = os.getenv("IXIA_IP", "127.0.0.1") + # E2E Controller + app.config["TFS_E2E_IP"] = os.getenv("TFS_E2E_IP", "127.0.0.1") + # WebUI app.config["WEBUI_DEPLOY"] = os.getenv("WEBUI_DEPLOY", "false").lower() == "true" diff --git a/src/main.py b/src/main.py index 6a90dc5..5354c38 100644 --- a/src/main.py +++ b/src/main.py @@ -89,6 +89,7 @@ class NSController: # Reset requests requests = {"services":[]} + response = None # Process intent (translate if 3GPP) ietf_intents = nbi_processor(intent_json) @@ -96,16 +97,22 @@ class NSController: for intent in ietf_intents: # Mapper rules = mapper(intent) - # Store slice request details and build response - store_data(intent, slice_id, controller_type=self.controller_type) + # Build response self.response = build_response(intent, self.response, controller_type= self.controller_type) # Realizer request = realizer(intent, controller_type=self.controller_type, response = self.response, rules = rules) - requests["services"].append(request) + # Store slice request details + if request: + requests["services"].append(request) + store_data(intent, slice_id, controller_type=self.controller_type) # Store the generated template for debugging dump_templates(intent_json, ietf_intents, requests) + # Check if there are services to process + if not requests.get("services"): + raise RuntimeError("No service to process.") + # Send config to controllers response = send_controller(self.controller_type, requests) diff --git a/src/mapper/main.py b/src/mapper/main.py index baeae47..27a9ef4 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -65,7 +65,8 @@ def mapper(ietf_intent): # Request the controller to create a new NRP that meets the SLOs answer = realizer(ietf_intent, True, "CREATE", best_nrp) if not answer: - raise Exception("Slice rejected due to lack of NRPs") + logging.error("Slice rejected due to lack of NRPs") + return None # TODO Here we should put how the slice is attached to the new nrp if current_app.config["PLANNER_ENABLED"]: diff --git a/src/planner/energy_planner/energy.py b/src/planner/energy_planner/energy.py index aee53bc..c44a210 100644 --- a/src/planner/energy_planner/energy.py +++ b/src/planner/energy_planner/energy.py @@ -17,14 +17,20 @@ import logging, random, os, json, heapq from src.config.constants import SRC_PATH from flask import current_app +from src.utils.safe_get import safe_get -def energy_planner(self, intent): - energy_metrics = self.__retrieve_energy() - topology = self.__retrieve_topology() - source = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[0].get("id") or "A" - destination = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[1].get("id") or "B" +def energy_planner(intent): + energy_metrics = retrieve_energy() + topology = retrieve_topology() + source = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 0, "node-id"]) + destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 1, "node-id"]) optimal_path = [] + allowed_ids = {"A", "B", "C", "D", "E", "F", "G"} + + if source not in allowed_ids or destination not in allowed_ids: + logging.warning(f"Topology not recognized (source: {source}, destination: {destination}). Skipping energy-based planning.") + return None # If using an external PCE if current_app.config["PCE_EXTERNAL"]: logging.debug("Using external PCE for path planning") @@ -121,37 +127,37 @@ def energy_planner(self, intent): "URE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "renewable_energy_usage"), None) } logging.debug(f"Planning optimal path from {source} to {destination} with DLOS: {dlos}") - optimal_path = self.__calculate_optimal_path(topology, energy_metrics, source, destination, dlos) + optimal_path = calculate_optimal_path(topology, energy_metrics, source, destination, dlos) if not optimal_path: - logging.error("No valid path found") - raise Exception("No valid energy path found") + logging.error("No valid energy path found") + return None return optimal_path -def __retrieve_energy(self): +def retrieve_energy(): # TODO : Implement the logic to retrieve energy consumption data from controller # Taking it from static file - with open(os.path.join(SRC_PATH, "planner/energy_ddbb.json"), "r") as archivo: + with open(os.path.join(SRC_PATH, "planner/energy_planner/energy_ddbb.json"), "r") as archivo: energy_metrics = json.load(archivo) return energy_metrics -def __retrieve_topology(self): +def retrieve_topology(): if current_app.config["PCE_EXTERNAL"]: # TODO : Implement the logic to retrieve topology data from external PCE # GET /sss/v1/topology/node and /sss/v1/topology/link - with open(os.path.join(SRC_PATH, "planner/ext_topo_ddbb.json"), "r") as archivo: + with open(os.path.join(SRC_PATH, "planner/energy_planner/ext_topo_ddbb.json"), "r") as archivo: topology = json.load(archivo) else: # TODO : Implement the logic to retrieve topology data from controller # Taking it from static file - with open(os.path.join(SRC_PATH, "planner/topo_ddbb.json"), "r") as archivo: + with open(os.path.join(SRC_PATH, "planner/energy_planner/topo_ddbb.json"), "r") as archivo: topology = json.load(archivo) return topology -def __calculate_optimal_path(self, topology, energy_metrics, source, destination, dlos): +def calculate_optimal_path(topology, energy_metrics, source, destination, dlos): logging.debug("Starting optimal path calculation...") # Create a dictionary with the weights of each node @@ -171,7 +177,7 @@ def __calculate_optimal_path(self, topology, energy_metrics, source, destination logging.debug(f"Node {node_id}: EC={ec}, CE={ce}, EE={ee}, URE={ure}") logging.debug(f"Node {node_id}: PS={total_power_supply}, BO={total_power_boards}, CO={total_power_components}, TR={total_power_transceivers}") - weight = self.__compute_node_weight(ec, ce, ee, ure, + weight = compute_node_weight(ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, @@ -254,7 +260,7 @@ def __calculate_optimal_path(self, topology, energy_metrics, source, destination return [] -def __compute_node_weight(self, ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, total_power_transceivers, alpha=1, beta=1, gamma=1, delta=1): +def compute_node_weight(ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, total_power_transceivers, alpha=1, beta=1, gamma=1, delta=1): """ Calcula el peso de un nodo con la fórmula: w(v) = α·EC + β·CE + γ/EE + δ·(1 - URE) diff --git a/src/planner/energy_ddbb.json b/src/planner/energy_planner/energy_ddbb.json similarity index 100% rename from src/planner/energy_ddbb.json rename to src/planner/energy_planner/energy_ddbb.json diff --git a/src/planner/ext_topo_ddbb.json b/src/planner/energy_planner/ext_topo_ddbb.json similarity index 100% rename from src/planner/ext_topo_ddbb.json rename to src/planner/energy_planner/ext_topo_ddbb.json diff --git a/src/planner/topo_ddbb.json b/src/planner/energy_planner/topo_ddbb.json similarity index 100% rename from src/planner/topo_ddbb.json rename to src/planner/energy_planner/topo_ddbb.json diff --git a/src/planner/hrat_planner/hrat.py b/src/planner/hrat_planner/hrat.py index c03560e..363d8fe 100644 --- a/src/planner/hrat_planner/hrat.py +++ b/src/planner/hrat_planner/hrat.py @@ -14,39 +14,42 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import logging -import requests - -def hrat_planner(data: str, action: str = "create") -> dict: - - data = {'network-slice-uuid': 'ecoc25-short-path-a7764e55-9bdb-4e38-9386-02ff47a33225', 'viability': True, 'actions': [{'type': 'CREATE_OPTICAL_SLICE', 'layer': 'OPTICAL', 'content': {'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd', 'service-interface-point': [{'uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a'}, {'uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625'}], 'node': [{'uuid': '68eb48ac-b686-5653-bdaf-7ccaeecd0709', 'owned-node-edge-point': [{'uuid': '7fd74b80-2b5a-55e2-8ef7-82bf589c9591', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '7b9f0b65-2387-5352-bc36-7173639463f0', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}, {'uuid': 'f55351ce-a5c8-50a7-b506-95b40e08bce4', 'owned-node-edge-point': [{'uuid': 'da6d924d-9cb4-5add-817d-f83e910beb2e', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '577ec899-ad92-5a19-a140-405a3cdbaa17', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}], 'link': [{'uuid': '3beef785-bb26-5741-af10-c5e1838c1701'}, {'uuid': '6144c664-246a-58ed-bf0a-7ec4286625da'}]}, 'controller-uuid': 'TAPI Optical Controller'}, {'type': 'PROVISION_MEDIA_CHANNEL_OLS_PATH', 'layer': 'OPTICAL', 'content': {'ols-path-uuid': 'cfeae4cb-c305-4884-9945-8b0c0f040c98', 'src-sip-uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a', 'dest-sip-uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625', 'direction': 'BIDIRECTIONAL', 'layer-protocol-name': 'PHOTONIC_MEDIA', 'layer-protocol-qualifier': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_MC', 'bandwidth-ghz': 100, 'link-uuid-path': ['3beef785-bb26-5741-af10-c5e1838c1701'], 'lower-frequency-mhz': '194700000', 'upper-frequency-mhz': '194800000', 'adjustment-granularity': 'G_6_25GHZ', 'grid-type': 'FLEX'}, 'controller-uuid': 'TAPI Optical Controller', 'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-1', 'termination-point-uuid': 'Ethernet110', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-2', 'termination-point-uuid': 'Ethernet220', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'CONFIG_VPNL3', 'layer': 'IP', 'content': {'tunnel-uuid': '9aae851a-eea9-4a28-969f-0e2c2196e936', 'src-node-uuid': 'Phoenix-1', 'src-ip-address': '10.10.1.1', 'src-ip-mask': '/24', 'src-vlan-id': 100, 'dest-node-uuid': 'Phoenix-2', 'dest-ip-address': '10.10.2.1', 'dest-ip-mask': '/24', 'dest-vlan-id': 100}, 'controller-uuid': 'IP Controller'}]} - return data - - # url = 'http://192.168.1.143:9090/api/resource-allocation/transport-network-slice-l3' - # headers = {'Content-Type': 'application/json'} - # try: - # if action == "delete": - # data = { - # "ietf-network-slice-service:network-slice-services": { - # "slice-service": [ - # { - # "id": data - # } - # ] - # } - # } - # response = requests.delete(url, headers=headers, json=data, timeout=15) - # elif action == "create": - # response = requests.post(url, headers=headers, json=data, timeout=15) - # else: - # raise ValueError("Invalid action. Use 'create' or 'delete'.") - # except requests.exceptions.RequestException as e: - # logging.error(f"HTTP request failed: {e}") - # return {} - - # # Check and return the response - # if response.ok: - # return response.json() - # else: - # print(f"Request failed with status code {response.status_code}: {response.text}") - # response.raise_for_status() +import logging, requests + +def hrat_planner(data: str, ip: str, action: str = "create") -> dict: + + data_static = {'network-slice-uuid': 'ecoc25-short-path-a7764e55-9bdb-4e38-9386-02ff47a33225', 'viability': True, 'actions': [{'type': 'CREATE_OPTICAL_SLICE', 'layer': 'OPTICAL', 'content': {'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd', 'service-interface-point': [{'uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a'}, {'uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625'}], 'node': [{'uuid': '68eb48ac-b686-5653-bdaf-7ccaeecd0709', 'owned-node-edge-point': [{'uuid': '7fd74b80-2b5a-55e2-8ef7-82bf589c9591', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '7b9f0b65-2387-5352-bc36-7173639463f0', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}, {'uuid': 'f55351ce-a5c8-50a7-b506-95b40e08bce4', 'owned-node-edge-point': [{'uuid': 'da6d924d-9cb4-5add-817d-f83e910beb2e', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}, {'uuid': '577ec899-ad92-5a19-a140-405a3cdbaa17', 'media-channel-node-edge-point-spec': {'mc-pool': {'supportable-spectrum': [{'lower-frequency': '191325000', 'upper-frequency': '192225000'}, {'lower-frequency': '194325000', 'upper-frequency': '195225000'}]}}}]}], 'link': [{'uuid': '3beef785-bb26-5741-af10-c5e1838c1701'}, {'uuid': '6144c664-246a-58ed-bf0a-7ec4286625da'}]}, 'controller-uuid': 'TAPI Optical Controller'}, {'type': 'PROVISION_MEDIA_CHANNEL_OLS_PATH', 'layer': 'OPTICAL', 'content': {'ols-path-uuid': 'cfeae4cb-c305-4884-9945-8b0c0f040c98', 'src-sip-uuid': 'e7444187-119b-5b2e-8a60-ee26b30c441a', 'dest-sip-uuid': 'b32b1623-1f64-59d2-8148-b035a8f77625', 'direction': 'BIDIRECTIONAL', 'layer-protocol-name': 'PHOTONIC_MEDIA', 'layer-protocol-qualifier': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_MC', 'bandwidth-ghz': 100, 'link-uuid-path': ['3beef785-bb26-5741-af10-c5e1838c1701'], 'lower-frequency-mhz': '194700000', 'upper-frequency-mhz': '194800000', 'adjustment-granularity': 'G_6_25GHZ', 'grid-type': 'FLEX'}, 'controller-uuid': 'TAPI Optical Controller', 'tenant-uuid': 'ea4ade23-1444-4f93-aabc-4fcbe2ae74dd'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-1', 'termination-point-uuid': 'Ethernet110', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'ACTIVATE_TRANSCEIVER', 'layer': 'OPTICAL', 'content': {'node-uuid': 'Phoenix-2', 'termination-point-uuid': 'Ethernet220', 'transceiver-type': 'CFP2', 'frequency-ghz': 194700.0, 'spectrum-width-ghz': 100.0, 'tx-power-dbm': 0.0}, 'controller-uuid': 'IP Controller'}, {'type': 'CONFIG_VPNL3', 'layer': 'IP', 'content': {'tunnel-uuid': '9aae851a-eea9-4a28-969f-0e2c2196e936', 'src-node-uuid': 'Phoenix-1', 'src-ip-address': '10.10.1.1', 'src-ip-mask': '/24', 'src-vlan-id': 100, 'dest-node-uuid': 'Phoenix-2', 'dest-ip-address': '10.10.2.1', 'dest-ip-mask': '/24', 'dest-vlan-id': 100}, 'controller-uuid': 'IP Controller'}]} + url = f'http://{ip}:9090/api/resource-allocation/transport-network-slice-l3' + headers = {'Content-Type': 'application/json'} + + try: + if action == "delete": + payload = { + "ietf-network-slice-service:network-slice-services": { + "slice-service": [ + { + "id": data + } + ] + } + } + response = requests.delete(url, headers=headers, json=payload, timeout=15) + elif action == "create": + response = requests.post(url, headers=headers, json=data, timeout=15) + else: + logging.error("Invalid action. Use 'create' or 'delete'.") + return data_static + + if response.ok: + return response.json() + else: + logging.error(f"Request failed with status code {response.status_code}: {response.text}") + return data_static + + except requests.exceptions.RequestException as e: + logging.error(f"HTTP request failed: {e}. Returning default data") + return data_static + except Exception as e: + logging.error(f"Unexpected error: {e}") + return data_static + diff --git a/src/planner/planner.py b/src/planner/planner.py index b44dc4c..b5ba22d 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -18,6 +18,7 @@ import logging from src.planner.energy_planner.energy import energy_planner from src.planner.hrat_planner.hrat import hrat_planner from src.planner.tfs_optical_planner.tfs_optical import tfs_optical_planner +from flask import current_app class Planner: @@ -31,6 +32,6 @@ class Planner: """ logging.info(f"Planner type selected: {type}") if type == "ENERGY" : return energy_planner(intent) - elif type == "HRAT" : return hrat_planner(intent) - elif type == "TFS_OPTICAL": return tfs_optical_planner(intent, action = "create") + elif type == "HRAT" : return hrat_planner(intent, current_app.config["HRAT_IP"]) + elif type == "TFS_OPTICAL": return tfs_optical_planner(intent, current_app.config["OPTICAL_PLANNER_IP"], action = "create") else : return None diff --git a/src/planner/tfs_optical_planner/tfs_optical.py b/src/planner/tfs_optical_planner/tfs_optical.py index cebc61b..41af023 100644 --- a/src/planner/tfs_optical_planner/tfs_optical.py +++ b/src/planner/tfs_optical_planner/tfs_optical.py @@ -19,16 +19,17 @@ import os import uuid import json from src.config.constants import TEMPLATES_PATH +from src.utils.safe_get import safe_get -def tfs_optical_planner(intent, action: str = "create") -> dict: +def tfs_optical_planner(intent, ip: str, action: str = "create") -> dict: if action == 'delete': - logging.info("DELETE REQUEST RECEIVED: %s", intent) + logging.debug("DELETE REQUEST RECEIVED: %s", intent) with open(os.path.join(TEMPLATES_PATH, "slice.db"), 'r', encoding='utf-8') as file: slices = json.load(file) for slice_obj in slices: if 'slice_id' in slice_obj and slice_obj['slice_id'] == intent: - logging.info("Slice found: %s", slice_obj['slice_id']) + logging.debug("Slice found: %s", slice_obj['slice_id']) source = None destination = None services = slice_obj['intent']['ietf-network-slice-service:network-slice-services']['slice-service'] @@ -59,26 +60,31 @@ def tfs_optical_planner(intent, action: str = "create") -> dict: for cg in c_groups: constructs = cg.get("connectivity-construct", []) for construct in constructs: - if "p2mp-sdp" in construct: - source = construct["p2mp-sdp"]["root-sdp-id"] - destination = construct["p2mp-sdp"]["leaf-sdp-id"] + source = safe_get(construct, ["p2mp-sdp", "root-sdp-id"]) + destination = safe_get(construct, ["p2mp-sdp", "leaf-sdp-id"]) + if source and destination: break if source and destination: break - - response = send_request(source, destination) - - summary = { - "source": source, - "destination": destination, - "connectivity-service": response - } - logging.info(summary) - rules = generate_rules(summary, intent,action) + response = None + if source and destination: + response = send_request(source, destination, ip) + if not response: + return None + summary = { + "source": source, + "destination": destination, + "connectivity-service": response + } + logging.debug(summary) + rules = generate_rules(summary, intent,action) + else: + logging.warning(f"No rules generated. Skipping optical planning.") + return None return rules -def send_request(source, destination): - url = "http://10.30.7.66:31060/OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp" +def send_request(source, destination, ip): + url = f"http://{ip}:31060/OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp" headers = { "Content-Type": "application/json", @@ -103,10 +109,14 @@ def send_request(source, destination): "band": 200, "subcarriers_per_source": [4] * len(sources_list) } - logging.info(f"Payload for path computation: {json.dumps(payload, indent=2)}") + logging.debug(f"Payload for path computation: {json.dumps(payload, indent=2)}") - response = requests.post(url, headers=headers, data=json.dumps(payload)) - return json.loads(response.text) + try: + response = requests.post(url, headers=headers, data=json.dumps(payload)) + return json.loads(response.text) + except requests.exceptions.RequestException: + logging.warning("Error connecting to the Optical Planner service. Skipping optical planning.") + return None def group_block(group, action, group_id_override=None, node = None): active = "true" if action == 'create' else "false" diff --git a/src/realizer/e2e/e2e_connect.py b/src/realizer/e2e/e2e_connect.py index ad100d3..b260dd5 100644 --- a/src/realizer/e2e/e2e_connect.py +++ b/src/realizer/e2e/e2e_connect.py @@ -14,36 +14,8 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import json -import logging -from flask import current_app -from src.utils.send_response import send_response +from ..tfs.helpers.tfs_connector import tfs_connector def e2e_connect(requests, controller_ip): - for request in requests["services"]: - for service in request: - logging.info(f"DATOS A ENVIAR: {service}") - # user="admin" - # password="admin" - # token="" - # session = requests.Session() - # session.auth = (user, password) - # url=f'http://{controller_ip}/webui' - # response=session.get(url=url) - # for item in response.iter_lines(): - # if"csrf_token" in str(item): - # string=str(item).split(' 0: rules = rules[0] - actions = rules.get("actions", []) + actions = rules.get("actions", []) if (rules and not type(rules)== str) else [] has_transceiver = any(a.get("type", "").startswith("XR_AGENT_ACTIVATE_TRANSCEIVER") for a in actions) has_optical = any(a.get("type", "").startswith("PROVISION_MEDIA_CHANNEL") for a in actions) @@ -66,7 +66,9 @@ def realizer(ietf_intent, need_nrp=False, order=None, nrp=None, controller_type= elif del_optical: selected_way = "DEL_OPTIC" elif del_l3: selected_way = "DEL_L3VPN" elif del_l2: selected_way = "DEL_L2VPN" - else: raise ValueError("Cannot determine the realization way from rules") + else: + logging.warning("Cannot determine the realization way from rules. Skipping request.") + return None way = selected_way else: way = safe_get(ietf_intent, ['ietf-network-slice-service:network-slice-services', 'slice-service', 0, 'service-tags', 'tag-type', 0, 'tag-type-value', 0]) diff --git a/src/realizer/send_controller.py b/src/realizer/send_controller.py index a59e7bb..01f9a95 100644 --- a/src/realizer/send_controller.py +++ b/src/realizer/send_controller.py @@ -31,4 +31,5 @@ def send_controller(controller_type, requests): logging.info("Requests sent to Ixia") elif controller_type == "E2E": response = e2e_connect(requests, current_app.config["TFS_E2E"]) + logging.info("Requests sent to Teraflow E2E") return response diff --git a/src/realizer/tfs/service_types/tfs_l2vpn.py b/src/realizer/tfs/service_types/tfs_l2vpn.py index ca190fb..2dab481 100644 --- a/src/realizer/tfs/service_types/tfs_l2vpn.py +++ b/src/realizer/tfs/service_types/tfs_l2vpn.py @@ -17,6 +17,7 @@ import logging, os from src.config.constants import TEMPLATES_PATH, NBI_L2_PATH from src.utils.load_template import load_template +from src.utils.safe_get import safe_get from ..helpers.cisco_connector import cisco_connector from flask import current_app @@ -41,9 +42,15 @@ def tfs_l2vpn(ietf_intent, response): """ # Hardcoded router endpoints # TODO (should be dynamically determined) - origin_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] + origin_router_id = safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 0, "attachment-circuits", "attachment-circuit", 0, "sdp-peering", "peer-sap-id"]) + if not origin_router_id: + logging.warning("Origin router ID not found in the intent. Skipping L2VPN realization.") + return None origin_router_if = '0/0/0-GigabitEthernet0/0/0/0' - destination_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] + destination_router_id = safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 1, "attachment-circuits", "attachment-circuit", 0, "sdp-peering", "peer-sap-id"]) + if not destination_router_id: + logging.warning("Destination router ID not found in the intent. Skipping L2VPN realization.") + return None destination_router_if = '0/0/0-GigabitEthernet0/0/0/0' id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] slice = next((d for d in response if d.get("id") == id), None) @@ -104,7 +111,7 @@ def tfs_l2vpn(ietf_intent, response): site["site-location"] = sdp["node-id"] site["site-network-access"]["interface"]["ip-address"] = sdp["sdp-ip-address"] - logging.info(f"L2VPN Intent realized\n") + logging.info(f"L2VPN Intent realized") return tfs_request def tfs_l2vpn_support(requests): diff --git a/src/realizer/tfs/service_types/tfs_l3vpn.py b/src/realizer/tfs/service_types/tfs_l3vpn.py index ff8f081..3a1f179 100644 --- a/src/realizer/tfs/service_types/tfs_l3vpn.py +++ b/src/realizer/tfs/service_types/tfs_l3vpn.py @@ -17,6 +17,7 @@ import logging, os from src.config.constants import TEMPLATES_PATH, NBI_L3_PATH from src.utils.load_template import load_template +from src.utils.safe_get import safe_get from flask import current_app def tfs_l3vpn(ietf_intent, response): @@ -39,9 +40,15 @@ def tfs_l3vpn(ietf_intent, response): """ # Hardcoded router endpoints # TODO (should be dynamically determined) - origin_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] + origin_router_id = safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 0, "attachment-circuits", "attachment-circuit", 0, "sdp-peering", "peer-sap-id"]) + if not origin_router_id: + logging.warning("Origin router ID not found in the intent. Skipping L3VPN realization.") + return None origin_router_if = '0/0/0-GigabitEthernet0/0/0/0' - destination_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] + destination_router_id = safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "sdps", "sdp", 1, "attachment-circuits", "attachment-circuit", 0, "sdp-peering", "peer-sap-id"]) + if not destination_router_id: + logging.warning("Destination router ID not found in the intent. Skipping L3VPN realization.") + return None destination_router_if = '0/0/0-GigabitEthernet0/0/0/0' id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] slice = next((d for d in response if d.get("id") == id), None) @@ -128,6 +135,6 @@ def tfs_l3vpn(ietf_intent, response): access["service"]["svc-mtu"] = int(cvalue) - logging.info(f"L3VPN Intent realized\n") + logging.info(f"L3VPN Intent realized") #self.answer[self.subnet]["VLAN"] = vlan_value return tfs_request \ No newline at end of file diff --git a/src/utils/build_response.py b/src/utils/build_response.py index 0f96cfa..7d67c8b 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -14,63 +14,47 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -def build_response(intent, response, controller_type = None): - - if controller_type == "E2E": - id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] - source = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["id"] - destination = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["id"] - vlan = None - qos_requirements = [] - - response.append({ - "id": id, - "source": source, - "destination": destination, - "vlan": vlan, - "requirements": qos_requirements, - }) - else: - id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] - source = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] - destination = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] - vlan = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] - - # Extract QoS Profile from intent - #QoSProfile = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] - - qos_requirements = [] +from .safe_get import safe_get - # Populate response with QoS requirements and VLAN from intent - slo_policy = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"] - - # Process metrics - for metric in slo_policy.get("metric-bound", []): - constraint_type = f"{metric['metric-type']}[{metric['metric-unit']}]" - constraint_value = str(metric["bound"]) - qos_requirements.append({ - "constraint_type": constraint_type, - "constraint_value": constraint_value - }) +def build_response(intent, response, controller_type = None): + """Build a structured response from the intent.""" + id = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"id"]) + source = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"id"]) + destination = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",1,"id"]) + vlan = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slice-service",0,"sdps","sdp",0,"service-match-criteria","match-criterion",0,"value"]) + + qos_requirements = [] + + # Populate response with QoS requirements and VLAN from intent + slo_policy = safe_get(intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates","slo-sle-template",0,"slo-policy"]) + + # Process metrics + for metric in slo_policy.get("metric-bound", []): + constraint_type = f"{metric['metric-type']}[{metric['metric-unit']}]" + constraint_value = str(metric["bound"]) + qos_requirements.append({ + "constraint_type": constraint_type, + "constraint_value": constraint_value + }) - # Availability - if "availability" in slo_policy: - qos_requirements.append({ - "constraint_type": "availability[%]", - "constraint_value": str(slo_policy["availability"]) - }) + # Availability + if "availability" in slo_policy: + qos_requirements.append({ + "constraint_type": "availability[%]", + "constraint_value": str(slo_policy["availability"]) + }) - # MTU - if "mtu" in slo_policy: - qos_requirements.append({ - "constraint_type": "mtu[bytes]", - "constraint_value": str(slo_policy["mtu"]) - }) - response.append({ - "id": id, - "source": source, - "destination": destination, - "vlan": vlan, - "requirements": qos_requirements, + # MTU + if "mtu" in slo_policy: + qos_requirements.append({ + "constraint_type": "mtu[bytes]", + "constraint_value": str(slo_policy["mtu"]) }) + response.append({ + "id": id, + "source": source, + "destination": destination, + "vlan": vlan, + "requirements": qos_requirements, + }) return response \ No newline at end of file diff --git a/swagger/E2E_namespace.py b/swagger/E2E_namespace.py index 86c9915..a53cd0d 100644 --- a/swagger/E2E_namespace.py +++ b/swagger/E2E_namespace.py @@ -53,6 +53,7 @@ class E2ESliceList(Resource): @e2e_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") @e2e_ns.response(201,"Slice created successfully", slice_response_model) + @e2e_ns.response(200, "No service to process.") @e2e_ns.response(400, "Invalid request format") @e2e_ns.response(500, "Internal server error") @e2e_ns.expect(upload_parser) diff --git a/swagger/ixia_namespace.py b/swagger/ixia_namespace.py index 3c16be1..e8f4ac9 100644 --- a/swagger/ixia_namespace.py +++ b/swagger/ixia_namespace.py @@ -52,6 +52,7 @@ class IxiaSliceList(Resource): @ixia_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") @ixia_ns.response(201, "Slice created successfully", slice_response_model) + @ixia_ns.response(200, "No service to process.") @ixia_ns.response(400, "Invalid request format") @ixia_ns.response(500, "Internal server error") @ixia_ns.expect(upload_parser) diff --git a/swagger/tfs_namespace.py b/swagger/tfs_namespace.py index 208da18..0916360 100644 --- a/swagger/tfs_namespace.py +++ b/swagger/tfs_namespace.py @@ -52,6 +52,7 @@ class TfsSliceList(Resource): @tfs_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") @tfs_ns.response(201,"Slice created successfully", slice_response_model) + @tfs_ns.response(200, "No service to process.") @tfs_ns.response(400, "Invalid request format") @tfs_ns.response(500, "Internal server error") @tfs_ns.expect(upload_parser) -- GitLab From d02c1886679afa1026e6590d186cd9759e4b52b1 Mon Sep 17 00:00:00 2001 From: velazquez Date: Tue, 14 Oct 2025 12:51:57 +0200 Subject: [PATCH 3/3] Add timeout to post requests --- src/planner/tfs_optical_planner/tfs_optical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/planner/tfs_optical_planner/tfs_optical.py b/src/planner/tfs_optical_planner/tfs_optical.py index 41af023..c8034cf 100644 --- a/src/planner/tfs_optical_planner/tfs_optical.py +++ b/src/planner/tfs_optical_planner/tfs_optical.py @@ -112,7 +112,7 @@ def send_request(source, destination, ip): logging.debug(f"Payload for path computation: {json.dumps(payload, indent=2)}") try: - response = requests.post(url, headers=headers, data=json.dumps(payload)) + response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=15) return json.loads(response.text) except requests.exceptions.RequestException: logging.warning("Error connecting to the Optical Planner service. Skipping optical planning.") -- GitLab