diff --git a/app.py b/app.py index ccdbed34dd7ca6ae99514881b848899731487b3e..ceec6aeb946d7dec9730b29d6cd4ee707b68b483 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/api/main.py b/src/api/main.py index 344171c982dc5400741a8c26cf9c8c8300ee5f8a..441bed094880d03fd1ea81a44250c457176cbcff 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 40376157ba67c9a7630c5f2dd3473e3110b10207..784d74ecd09329a1c89b008c495910a1592ad372 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 6d0f8d32a25f1a0cfed371c63dff1d885127a768..04b72aae571b67b8e490a01267c4af5aac4e12d9 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -42,7 +42,10 @@ 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" + 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" @@ -55,6 +58,9 @@ def create_config(app: Flask): # 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 076c8434171c29c0d487aead30431ac956ce1193..5354c38df986817c1256f5548667d2b88c78d9bb 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,43 @@ class NSController: Returns: tuple: Response status and HTTP status code - + """ # Start performance tracking self.start_time = time.perf_counter() # Reset requests requests = {"services":[]} - + response = None + # Process intent (translate if 3GPP) ietf_intents = nbi_processor(intent_json) - for intent in ietf_intents: + for intent in ietf_intents: # Mapper - 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) + rules = mapper(intent) + # 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) - requests["services"].append(request) - + request = realizer(intent, controller_type=self.controller_type, response = self.response, rules = rules) + # 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) 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 b7389356ba436ffa6a72afa04b9433eb8dfbe308..27a9ef4bd39183a809b0cbbed24aaf1d34db53ec 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,16 @@ 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") + 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"]: - 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 0000000000000000000000000000000000000000..c44a210d8b6dcdad7596f10eba37dbd894ba2851 --- /dev/null +++ b/src/planner/energy_planner/energy.py @@ -0,0 +1,281 @@ +# 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 +from src.utils.safe_get import safe_get + + +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") + 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 = calculate_optimal_path(topology, energy_metrics, source, destination, dlos) + + if not optimal_path: + logging.error("No valid energy path found") + return None + + return optimal_path + +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_planner/energy_ddbb.json"), "r") as archivo: + energy_metrics = json.load(archivo) + return energy_metrics + +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/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/energy_planner/topo_ddbb.json"), "r") as archivo: + topology = json.load(archivo) + return topology + + + +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 + 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 = 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(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/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 new file mode 100644 index 0000000000000000000000000000000000000000..363d8fe412770b95a69609baee662e7e0380640d --- /dev/null +++ b/src/planner/hrat_planner/hrat.py @@ -0,0 +1,55 @@ +# 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, 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 c2613bd9c7644dd9a91cda04c790156385da9ab2..b5ba22d6920c121668608515f08a73db9dc45fc5 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -14,269 +14,24 @@ # 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 +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: """ 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, 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 new file mode 100644 index 0000000000000000000000000000000000000000..c8034cfa44fdfc0ec82e9ce1818716ac6fd274ed --- /dev/null +++ b/src/planner/tfs_optical_planner/tfs_optical.py @@ -0,0 +1,264 @@ +# 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 +from src.utils.safe_get import safe_get + +def tfs_optical_planner(intent, ip: str, action: str = "create") -> dict: + if action == 'delete': + 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.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'] + 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: + 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 = 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, ip): + url = f"http://{ip}: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.debug(f"Payload for path computation: {json.dumps(payload, indent=2)}") + + try: + 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.") + return None + +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 0000000000000000000000000000000000000000..b260dd5bca6550714a0de12ca0251479e0589878 --- /dev/null +++ b/src/realizer/e2e/e2e_connect.py @@ -0,0 +1,21 @@ +# 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. + +from ..tfs.helpers.tfs_connector import tfs_connector + +def e2e_connect(requests, controller_ip): + response = tfs_connector().webui_post(controller_ip, requests) + return response \ No newline at end of file diff --git a/src/realizer/e2e/main.py b/src/realizer/e2e/main.py new file mode 100644 index 0000000000000000000000000000000000000000..6eed88389d428755c5b124a59383827de53d9259 --- /dev/null +++ b/src/realizer/e2e/main.py @@ -0,0 +1,28 @@ +# 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 +from .service_types.del_l3ipowdm_slice import del_l3ipowdm_slice +from .service_types.l3ipowdm_slice import l3ipowdm_slice + +def e2e(ietf_intent, way=None, response=None, rules = None): + logging.debug(f"E2E Realizer selected: {way}") + if way == "L3oWDM": realizing_request = l3ipowdm_slice(rules) + elif way == "DEL_L3oWDM": realizing_request = del_l3ipowdm_slice(rules, response) + else: + logging.warning(f"Unsupported way: {way}.") + realizing_request = None + return realizing_request \ No newline at end of file diff --git a/src/realizer/e2e/service_types/del_l3ipowdm_slice.py b/src/realizer/e2e/service_types/del_l3ipowdm_slice.py new file mode 100644 index 0000000000000000000000000000000000000000..8bbeb13c4fe8b58f2e3afb4ed5a22bfb02cb286c --- /dev/null +++ b/src/realizer/e2e/service_types/del_l3ipowdm_slice.py @@ -0,0 +1,177 @@ +# 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 includes original contributions from Telefonica Innovación Digital S.L. + +import logging, os +from src.config.constants import TEMPLATES_PATH, NBI_L2_PATH +from src.utils.load_template import load_template +from flask import current_app + +def del_l3ipowdm_slice(ietf_intent, response): + """ + Translate slice intent into a TeraFlow service request. + + This method prepares a L2VPN service request by: + 1. Defining endpoint routers + 2. Loading a service template + 3. Generating a unique service UUID + 4. Configuring service endpoints + 5. Adding QoS constraints + 6. Preparing configuration rules for network interfaces + + Args: + ietf_intent (dict): IETF-formatted network slice intent. + + Returns: + dict: A TeraFlow service request for L2VPN configuration. + + """ + # 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_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_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) + + if current_app.config["UPLOAD_TYPE"] == "WEBUI": + # Load L2VPN service template + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "L2-VPN_template_empty.json"))["services"][0] + + # Configure service UUID + tfs_request["service_id"]["service_uuid"]["uuid"] = ietf_intent['ietf-network-slice-service:network-slice-services']['slice-service'][0]["id"] + + # Configure service endpoints + for endpoint in tfs_request["service_endpoint_ids"]: + endpoint["device_id"]["device_uuid"]["uuid"] = origin_router_id if endpoint is tfs_request["service_endpoint_ids"][0] else destination_router_id + endpoint["endpoint_uuid"]["uuid"] = origin_router_if if endpoint is tfs_request["service_endpoint_ids"][0] else destination_router_if + + # Add service constraints + for constraint in slice.get("requirements", []): + tfs_request["service_constraints"].append({"custom": constraint}) + + # Add configuration rules + for i, config_rule in enumerate(tfs_request["service_config"]["config_rules"][1:], start=1): + router_id = origin_router_id if i == 1 else destination_router_id + router_if = origin_router_if if i == 1 else destination_router_if + resource_value = config_rule["custom"]["resource_value"] + + sdp_index = i - 1 + vlan_value = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][sdp_index]["service-match-criteria"]["match-criterion"][0]["value"] + if vlan_value: + resource_value["vlan_id"] = int(vlan_value) + resource_value["circuit_id"] = vlan_value + resource_value["remote_router"] = destination_router_id if i == 1 else origin_router_id + resource_value["ni_name"] = 'ELAN{:s}'.format(str(vlan_value)) + config_rule["custom"]["resource_key"] = f"/device[{router_id}]/endpoint[{router_if}]/settings" + + elif current_app.config["UPLOAD_TYPE"] == "NBI": + #self.path = NBI_L2_PATH + # Load IETF L2VPN service template + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "ietfL2VPN_template_empty.json")) + + # Add path to the request + tfs_request["path"] = NBI_L2_PATH + + # Generate service UUID + full_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] + uuid_only = full_id.split("slice-service-")[-1] + tfs_request["ietf-l2vpn-svc:vpn-service"][0]["vpn-id"] = uuid_only + + # Configure service endpoints + sites = tfs_request["ietf-l2vpn-svc:vpn-service"][0]["site"] + sdps = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"] + + for i, site in enumerate(sites): + is_origin = (i == 0) + router_id = origin_router_id if is_origin else destination_router_id + sdp = sdps[0] if is_origin else sdps[1] + site["site-id"] = router_id + site["site-location"] = sdp["node-id"] + site["site-network-access"]["interface"]["ip-address"] = sdp["sdp-ip-address"] + + logging.info(f"L2VPN Intent realized\n") + return tfs_request + +def tfs_l2vpn_support(requests): + """ + Configuration support for L2VPN with path selection based on MPLS traffic-engineering tunnels + + Args: + requests (list): A list of configuration parameters. + + """ + sources={ + "source": "10.60.125.44", + "config":[] + } + destinations={ + "destination": "10.60.125.45", + "config":[] + } + for request in requests: + # Configure Source Endpoint + temp_source = request["service_config"]["config_rules"][1]["custom"]["resource_value"] + endpoints = request["service_endpoint_ids"] + config = { + "ni_name": temp_source["ni_name"], + "remote_router": temp_source["remote_router"], + "interface": endpoints[0]["endpoint_uuid"]["uuid"].replace("0/0/0-", ""), + "vlan" : temp_source["vlan_id"], + "number" : temp_source["vlan_id"] % 10 + 1 + } + sources["config"].append(config) + + # Configure Destination Endpoint + temp_destiny = request["service_config"]["config_rules"][2]["custom"]["resource_value"] + config = { + "ni_name": temp_destiny["ni_name"], + "remote_router": temp_destiny["remote_router"], + "interface": endpoints[1]["endpoint_uuid"]["uuid"].replace("0/0/3-", ""), + "vlan" : temp_destiny["vlan_id"], + "number" : temp_destiny["vlan_id"] % 10 + 1 + } + destinations["config"].append(config) + + #cisco_source = cisco_connector(source_address, ni_name, remote_router, vlan, vlan % 10 + 1) + cisco_source = cisco_connector(sources["source"], sources["config"]) + commands = cisco_source.full_create_command_template() + cisco_source.execute_commands(commands) + + #cisco_destiny = cisco_connector(destination_address, ni_name, remote_router, vlan, vlan % 10 + 1) + cisco_destiny = cisco_connector(destinations["destination"], destinations["config"]) + commands = cisco_destiny.full_create_command_template() + cisco_destiny.execute_commands(commands) + +def tfs_l2vpn_delete(): + """ + Delete L2VPN configurations from Cisco devices. + + This method removes L2VPN configurations from Cisco routers + + Notes: + - Uses cisco_connector to generate and execute deletion commands + - Clears Network Interface (NI) settings + """ + # Delete Source Endpoint Configuration + source_address = "10.60.125.44" + cisco_source = cisco_connector(source_address) + cisco_source.execute_commands(cisco_source.create_command_template_delete()) + + # Delete Destination Endpoint Configuration + destination_address = "10.60.125.45" + cisco_destiny = cisco_connector(destination_address) + cisco_destiny.execute_commands(cisco_destiny.create_command_template_delete()) \ No newline at end of file diff --git a/src/realizer/e2e/service_types/l3ipowdm_slice.py b/src/realizer/e2e/service_types/l3ipowdm_slice.py new file mode 100644 index 0000000000000000000000000000000000000000..4fadf4487056d20f65941c0e0e45caf69b249a82 --- /dev/null +++ b/src/realizer/e2e/service_types/l3ipowdm_slice.py @@ -0,0 +1,192 @@ +# 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 includes original contributions from Telefonica Innovación Digital S.L. + +import logging, os +from src.config.constants import TEMPLATES_PATH +from src.utils.load_template import load_template + +def l3ipowdm_slice(rules): + """ + Prepare a Optical service request for an optical slice. + + This method prepares a TeraFlow service request for an optical slice by: + 1. Defining endpoint routers + 2. Loading a service template + 3. Generating a unique service UUID + 4. Configuring service endpoints + 5. Adding QoS constraints + + Args: + ietf_intent (dict): IETF-formatted network slice intent. + rules (dict, optional): Configuration rules for the optical slice. + + Returns: + dict: A TeraFlow service request for optical slice configuration. + """ + transceiver_params = [] + bandwidth = 0 + + logging.debug(f"Preparing L3oWDM slice with rules: {rules}") + tfs_requests = [] + for rule in rules["actions"]: + logging.debug(f"Processing rule: {rule['type']}") + if rule["type"] == "CREATE_OPTICAL_SLICE": + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "Optical_slice.json")) + request = optical_slice_template(tfs_request, rules) + logging.debug(f"Sending Optical Slice to Optical Controller {request}") + tfs_requests.append(request) + + elif rule["type"] == "PROVISION_MEDIA_CHANNEL_OLS_PATH": + + origin_router_id = rule["content"]["src-sip-uuid"] + destination_router_id = rule["content"]["dest-sip-uuid"] + direction = rule["content"]["direction"] + bandwidth = rule["content"]["bandwidth-ghz"] + service_uuid = rule["content"]["ols-path-uuid"] + tenant_uuid = rule["tenant-uuid"] + layer_protocol_name = rule["content"]["layer-protocol-name"] + layer_protocol_qualifier = rule["content"]["layer-protocol-qualifier"] + lower_frequency_mhz = rule["content"]["lower-frequency-mhz"] + upper_frequency_mhz = rule["content"]["upper-frequency-mhz"] + link_uuid_path = rule["content"]["link-uuid-path"] + granularity = rule["content"]["adjustment-granularity"] + grid = rule["content"]["grid-type"] + + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "TAPI_service.json")) + + tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid + config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + + config_rules["tapi_lsp"]["rule_set"]["src"] = origin_router_id + config_rules["tapi_lsp"]["rule_set"]["dst"] = destination_router_id + config_rules["tapi_lsp"]["rule_set"]["uuid"] = service_uuid + config_rules["tapi_lsp"]["rule_set"]["bw"] = str(bandwidth) + config_rules["tapi_lsp"]["rule_set"]["tenant_uuid"] = tenant_uuid + config_rules["tapi_lsp"]["rule_set"]["direction"] = direction + config_rules["tapi_lsp"]["rule_set"]["layer_protocol_name"] = layer_protocol_name + config_rules["tapi_lsp"]["rule_set"]["layer_protocol_qualifier"] = layer_protocol_qualifier + config_rules["tapi_lsp"]["rule_set"]["lower_frequency_mhz"] = str(lower_frequency_mhz) + config_rules["tapi_lsp"]["rule_set"]["upper_frequency_mhz"] = str(upper_frequency_mhz) + config_rules["tapi_lsp"]["rule_set"]["link_uuid_path"] = link_uuid_path + config_rules["tapi_lsp"]["rule_set"]["granularity"] = granularity + config_rules["tapi_lsp"]["rule_set"]["grid_type"] = grid + + logging.debug(f"Sending Media Channel Service to Orchestrator: {tfs_request}") + tfs_requests.append(tfs_request) + + elif rule["type"] == "ACTIVATE_TRANSCEIVER": + params = { + "router_id": rule["content"]["node-uuid"], + "router_tp": rule["content"]["termination-point-uuid"], + "frequency": rule["content"]["frequency-ghz"], + "power": rule["content"]["tx-power-dbm"] + } + transceiver_params.append(params) + elif rule["type"] == "CONFIG_VPNL3": + src_router_id = rule["content"]["src-node-uuid"] + + if src_router_id == transceiver_params[0]["router_id"]: + src_power = transceiver_params[0]["power"] + src_frequency = transceiver_params[0]["frequency"] + dst_power = transceiver_params[1]["power"] + dst_frequency = transceiver_params[1]["frequency"] + else: + src_power = transceiver_params[1]["power"] + src_frequency = transceiver_params[1]["frequency"] + dst_power = transceiver_params[0]["power"] + dst_frequency = transceiver_params[0]["frequency"] + + src_router_id = rule["content"]["src-node-uuid"] + src_ip_address = rule["content"]["src-ip-address"] + src_ip_mask = rule["content"]["src-ip-mask"] + src_vlan_id = rule["content"]["src-vlan-id"] + + dst_router_id = rule["content"]["dest-node-uuid"] + dst_ip_address = rule["content"]["dest-ip-address"] + dst_ip_mask = rule["content"]["dest-ip-mask"] + dst_vlan_id = rule["content"]["dest-vlan-id"] + + service_uuid = rule["content"]["tunnel-uuid"] + + tfs_request = load_template(os.path.join(TEMPLATES_PATH, "IPoWDM_orchestrator.json")) + tfs_request["services"][0]["service_id"]["service_uuid"]["uuid"] = service_uuid + config_rules = tfs_request["services"][0]["service_config"]["config_rules"][0] + src = config_rules["ipowdm"]["rule_set"]["src"] + src.append({ + 'uuid': src_router_id, + 'ip_address': src_ip_address, + 'ip_mask': src_ip_mask, + 'vlan_id': src_vlan_id, + 'power': src_power, + 'frequency': src_frequency + }) + + dst = config_rules["ipowdm"]["rule_set"]["dst"] + dst.append({ + 'uuid': dst_router_id, + 'ip_address': dst_ip_address, + 'ip_mask': dst_ip_mask, + 'vlan_id': dst_vlan_id, + 'power': dst_power, + 'frequency': dst_frequency + }) + + config_rules["ipowdm"]["rule_set"]["bw"] = bandwidth + config_rules["ipowdm"]["rule_set"]["uuid"] = service_uuid + + logging.debug(f"Sending IPoWDM Service to Orchestrator: {tfs_request}") + tfs_requests.append(tfs_request) + + else: + logging.debug("Unsupported rule type for optical slice: %s", rule["type"]) + return tfs_requests + +def optical_slice_template(template, rule): + """ + Complete the optical slice template with the data provided. + Args: + template (dict): optical slice template. + data (dict): Data to complete the template. + Returns: + dict: Template completed. + """ + + for action in rule.get('actions', []): + content = action.get('content', {}) + nodes = content.get('node', []) + for node in nodes: + for onp in node.get('owned-node-edge-point', []): + if 'media-channel-node-edge-point-spec' in onp: + onp['tapi-photonic-media:media-channel-node-edge-point-spec'] = onp.pop('media-channel-node-edge-point-spec') + + for i, sip in enumerate(template['tapi-common:context']['service-interface-point']): + if i < len(rule['actions'][0]['content']['service-interface-point']): + sip['uuid'] = rule['actions'][0]['content']['service-interface-point'][i]['uuid'] + + nodes_template = template['tapi-common:context']['tapi-topology:topology-context']['topology'][0]['node'] + nodes_data = rule['actions'][0]['content']['node'] + for new_node in nodes_data: + nodes_template.append(new_node) + + links_template = template['tapi-common:context']['tapi-topology:topology-context']['topology'][0]['link'] + links_rule = rule['actions'][0]['content']['link'] + for link_t in links_rule: + links_template.append(link_t) + + template['tapi-common:context']['uuid'] = rule['actions'][0]['content']['tenant-uuid'] + template['tapi-common:context']['name'][0]['value'] = rule['network-slice-uuid'] + + return template diff --git a/src/realizer/main.py b/src/realizer/main.py index e1f08a0ae6c6c899f6d7784cee1fd50e69a5eb45..d88a624148462213c3d4a970b6b32a15278d297c 100644 --- a/src/realizer/main.py +++ b/src/realizer/main.py @@ -19,7 +19,7 @@ from .select_way import select_way from .nrp_handler import nrp_handler from src.utils.safe_get import safe_get -def realizer(ietf_intent, need_nrp=False, order=None, nrp=None, controller_type=None, response=None): +def realizer(ietf_intent, need_nrp=False, order=None, nrp=None, controller_type=None, response=None, rules = None): """ Manage the slice creation workflow. @@ -39,6 +39,39 @@ def realizer(ietf_intent, need_nrp=False, order=None, nrp=None, controller_type= return nrp_view else: # Select slice service method - way = safe_get(ietf_intent, ['ietf-network-slice-service:network-slice-services', 'slice-service', 0, 'service-tags', 'tag-type', 0, 'tag-type-value', 0]) - request = select_way(controller=controller_type, way=way, ietf_intent=ietf_intent, response=response) + if controller_type == "E2E": + if isinstance(rules, list) and len(rules) > 0: rules = rules[0] + 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) + 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: + 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]) + 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 110b18ab77f278fd17eed453c7b92e599482d2ef..2b9d2da3836fc6e82600c9f3f33f261704f4ed57 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 9bb81afd09bd25e38458b8bce534a4c591c9e65f..01f9a95468bba1843291ec2e8ac40b3c01b3bade 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,7 @@ 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"]) + 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 ca190fbc84bee65dba713d0690f8e10e26c035a6..2dab48102343bbb34a07594811c93afcdac3b74e 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 ff8f0816a6697a9c48fd9c8c2fc9e3739c13cdd1..3a1f179a9ba471135e571d588ab0bf8ba0fdde8f 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/templates/IPoWDM_orchestrator.json b/src/templates/IPoWDM_orchestrator.json new file mode 100644 index 0000000000000000000000000000000000000000..60eb0dbaaebca3e0a35d5b9007121119a60443f8 --- /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 0000000000000000000000000000000000000000..94c87fe03f52b7a10acfc537eb5dbabfc6b4a46b --- /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 0000000000000000000000000000000000000000..1b09a04cb809d0d959340e6d23a15d65f7f372b0 --- /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 c013602b1d09d9beedb1f9297e73f54ba608d66c..7d67c8b60a750b47b7efedf3c9d3b2fcd5a9ad62 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -14,21 +14,20 @@ # 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"] +from .safe_get import safe_get - # Extract QoS Profile from intent - #QoSProfile = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] +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 = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"] - + 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']}]" diff --git a/swagger/E2E_namespace.py b/swagger/E2E_namespace.py new file mode 100644 index 0000000000000000000000000000000000000000..a53cd0de84a58344d9a5401a16bd04739179a6cf --- /dev/null +++ b/swagger/E2E_namespace.py @@ -0,0 +1,153 @@ +# 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(200, "No service to process.") + @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 diff --git a/swagger/ixia_namespace.py b/swagger/ixia_namespace.py index 3c16be184f8822146c0f61ccc229159feec10bd0..e8f4ac9c04f97c832f78828a48ec0a637b675475 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 208da1852b5ba7ac8cd21c18ec077f1be91e01c3..09163602aeca9bb14521b272bcfa980093a9c5f2 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)