Loading src/api/main.py +3 −15 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. from src.utils.send_response import send_response import logging, json, time, requests, traceback, asyncio, aiohttp import logging, json, asyncio from flask import current_app from src.database.db import get_data, delete_data, get_all_data, delete_all_data from src.database.service_db import delete_by_slice_id, get_data_by_slice_id Loading @@ -26,7 +26,6 @@ from src.utils.safe_get import safe_get from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data from typing import Dict, Tuple from src.realizer.restconf.connectors.tfs_connector import tfs_connector as tfs_restconf_connector from src.planner.shortest_path import get_shortest_path from src.realizer.tfs.service_types.tfs_l2vpn import tfs_l2vpn_delete Loading Loading @@ -1158,12 +1157,6 @@ class Api: logging.debug(f"Getting telemetry for slice_id: {slice_id}") try: if slice_id is not None: slice_sdps = get_data_store(f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']/sdps") logging.debug(f"SDPs for slice_id '{slice_id}': {slice_sdps}") if not slice_sdps: raise ValueError("No SDPs found") if len(slice_sdps) > 2: raise Exception(f"Monitoring for more than 2 SDPs is not supported. Found {len(slice_sdps)} SDPs.") xpath = f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']" existing_slice = get_data_store(xpath) if not existing_slice: Loading @@ -1174,7 +1167,7 @@ class Api: slo_sle_template = next(iter(slo_sle_template), None) if not slo_sle_template: raise ValueError(f"SLO/SLE template '{template_id}' not found for slice '{slice_id}'") metrics = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) metrics = self.slice_service.monitoring(slice_id, slo_sle_template) return metrics, 200 telemetry_data = {} Loading @@ -1191,12 +1184,7 @@ class Api: if not slo_sle_template: raise ValueError(f"SLO/SLE template '{selected_template_id}' not found for slice '{slice['id']}'") slice_id = slice["id"] slice_sdps = get_data_store(f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']/sdps") if not slice_sdps: raise ValueError("No SDPs found") if len(slice_sdps) > 2: raise Exception(f"Monitoring for more than 2 SDPs is not supported. Found {len(slice_sdps)} SDPs.") telemetry_data[slice_id] = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) telemetry_data[slice_id] = self.slice_service.monitoring(slice_id, slo_sle_template) return telemetry_data, 200 except ValueError as e: Loading src/main.py +2 −4 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ from src.database.store_data import store_data from src.database.service_db import delete_data from src.mapper.main import mapper from src.realizer.main import realizer from src.planner.shortest_path import get_shortest_path from src.realizer.send_controller import send_controller class NSController: Loading Loading @@ -197,7 +196,7 @@ class NSController: "setup_time": setup_time } def monitoring(self, slice_id, slo_sle_template, sdps): def monitoring(self, slice_id, slo_sle_template): """ Monitor the status of a specific network slice. Loading @@ -219,8 +218,7 @@ class NSController: """ payload = { "slice_id": slice_id, "slo_sle_template": slo_sle_template, "sdps": sdps "slo_sle_template": slo_sle_template } # Request the realizer to retrieve the metrics for the links of the specified slice Loading src/planner/shortest_path.py +2 −2 Original line number Diff line number Diff line Loading @@ -35,7 +35,7 @@ def get_shortest_path(network, src_node_id, dst_node_id, directed_graph: bool = nodes = network["node"] links = network["ietf-network-topology:link"] # Construir grafo # Build graph graph = { normalize_node_id(node["node-id"]): set() for node in nodes Loading Loading @@ -77,7 +77,7 @@ def get_shortest_path(network, src_node_id, dst_node_id, directed_graph: bool = if not visited[destination_node_idx]: return {"message": "No path found"}, 404 # Reconstruir camino # Rebuild path path = [] at = destination_node_idx Loading src/realizer/main.py +10 −16 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ from .select_way import select_way from .nrp_handler import nrp_handler from .get_metrics import get_metrics from src.utils.safe_get import safe_get from src.planner.shortest_path import get_shortest_path from src.database.service_db import get_data_by_slice_id from src.realizer.restconf.connectors.tfs_connector import tfs_connector from flask import current_app Loading Loading @@ -90,20 +90,14 @@ def realizer(payload, need_nrp=False, order=None, nrp=None, controller_type=None elif action == "MONITOR": logging.debug("Realizer action: MONITOR") slice_id = payload.get("slice_id", None) sdps = payload.get("sdps", None) sdps_object = safe_get(sdps, ['network-slice-services', 'slice-service']) sdps_object = sdps_object[slice_id]['sdps']['sdp'] sdp_ids = [sdp["id"] for sdp in sdps_object] # Retrieve Network topology topology, response = tfs_connector().get_network_topology(current_app.config["RESTCONF_IP"],slice_id) service_data = get_data_by_slice_id(slice_id) if len(service_data) > 1: raise ValueError(f"Slices with multiple associated services are not supported: '{slice_id}'") service_id = service_data[0]["service_id"] # Retrieve Service Path path, response = tfs_connector().get_service_path(current_app.config["RESTCONF_IP"], service_id) if response == 200: logging.debug(f"Retrieved topology for slice '{slice_id}'") # Get shortest path path, response = get_shortest_path(topology, sdp_ids[0], sdp_ids[1]) if response == 200: logging.debug(f"Retrieved shortest path for slice '{slice_id}': {path}") logging.debug(f"Retrieved service path for slice '{slice_id}' (service '{service_id}'): {path}") get_metrics(path, slice_id, controller_type) else: raise Exception("Error: Shortest path not retrieved ") else: raise Exception("Error: Topology not retrieved") No newline at end of file raise Exception("Error: Service path not retrieved") No newline at end of file src/realizer/restconf/connectors/tfs_connector.py +65 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import logging, requests, json, aiohttp, asyncio, threading from src.config.constants import NBI_L2_PATH, NBI_L3_PATH, NBI_IETF_NETWORKS_PATH, NBI_SIMAP_SUSCRIPTION_PATH from typing import Dict, Tuple from typing import Dict, Tuple, List # Temp until moving to .env SDN_SUBSCRIPTION_PERIOD = 10 # seconds Loading Loading @@ -141,6 +141,70 @@ class tfs_connector(): return network, 200 def get_device_name(self, tfs_ip: str, device_uuid: str) -> str: """ Get device name for a given device_uuid from TFS. Args: tfs_ip (str): IP address of the TFS instance device_uuid (str): Device UUID Returns: str: Name of the device """ url = f"http://{tfs_ip}/tfs-api/device/{device_uuid}" headers = {'accept': 'application/json'} response = requests.get(url, headers=headers, timeout=60) response.raise_for_status() device_data = response.json() return device_data.get("name", "") def get_service_path(self, tfs_ip: str, service_id: str) -> Tuple[List[str], int]: """ Get ordered list of node names along the service path from TFS. Args: tfs_ip (str): IP address of the TFS instance service_id (str): Service UUID Returns: Tuple[List[str], int]: List of node names in order and status code """ url = f'http://{tfs_ip}/tfs-api/context/admin/service/{service_id}/connections' headers = {'accept': 'application/json'} try: response = requests.get(url, headers=headers, timeout=60) if response.status_code == 200: data = response.json() connections = data.get("connections", []) raw_uuids = [] for conn in connections: for hop in conn.get("path_hops_endpoint_ids", []): device_id = hop.get("device_id", {}).get("device_uuid", {}).get("uuid") if device_id and (not raw_uuids or raw_uuids[-1] != device_id): raw_uuids.append(device_id) name_cache = {} path = [] for dev_uuid in raw_uuids: if dev_uuid not in name_cache: try: dev_name = self.get_device_name(tfs_ip, dev_uuid) name_cache[dev_uuid] = dev_name if dev_name else dev_uuid except Exception as e: logging.warning(f"Could not retrieve device name for '{dev_uuid}': {e}") name_cache[dev_uuid] = dev_uuid path.append(name_cache[dev_uuid]) logging.debug(f"Retrieved service path for service '{service_id}': {path}") return path, 200 else: logging.error(f"Failed to retrieve service path for service '{service_id}': status {response.status_code}") return [], response.status_code except Exception as e: logging.exception(f"Error retrieving service path for service '{service_id}': {e}") return [], 500 # --- SDN STREAMS --- async def get_session(self): Loading Loading
src/api/main.py +3 −15 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. from src.utils.send_response import send_response import logging, json, time, requests, traceback, asyncio, aiohttp import logging, json, asyncio from flask import current_app from src.database.db import get_data, delete_data, get_all_data, delete_all_data from src.database.service_db import delete_by_slice_id, get_data_by_slice_id Loading @@ -26,7 +26,6 @@ from src.utils.safe_get import safe_get from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data from typing import Dict, Tuple from src.realizer.restconf.connectors.tfs_connector import tfs_connector as tfs_restconf_connector from src.planner.shortest_path import get_shortest_path from src.realizer.tfs.service_types.tfs_l2vpn import tfs_l2vpn_delete Loading Loading @@ -1158,12 +1157,6 @@ class Api: logging.debug(f"Getting telemetry for slice_id: {slice_id}") try: if slice_id is not None: slice_sdps = get_data_store(f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']/sdps") logging.debug(f"SDPs for slice_id '{slice_id}': {slice_sdps}") if not slice_sdps: raise ValueError("No SDPs found") if len(slice_sdps) > 2: raise Exception(f"Monitoring for more than 2 SDPs is not supported. Found {len(slice_sdps)} SDPs.") xpath = f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']" existing_slice = get_data_store(xpath) if not existing_slice: Loading @@ -1174,7 +1167,7 @@ class Api: slo_sle_template = next(iter(slo_sle_template), None) if not slo_sle_template: raise ValueError(f"SLO/SLE template '{template_id}' not found for slice '{slice_id}'") metrics = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) metrics = self.slice_service.monitoring(slice_id, slo_sle_template) return metrics, 200 telemetry_data = {} Loading @@ -1191,12 +1184,7 @@ class Api: if not slo_sle_template: raise ValueError(f"SLO/SLE template '{selected_template_id}' not found for slice '{slice['id']}'") slice_id = slice["id"] slice_sdps = get_data_store(f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']/sdps") if not slice_sdps: raise ValueError("No SDPs found") if len(slice_sdps) > 2: raise Exception(f"Monitoring for more than 2 SDPs is not supported. Found {len(slice_sdps)} SDPs.") telemetry_data[slice_id] = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) telemetry_data[slice_id] = self.slice_service.monitoring(slice_id, slo_sle_template) return telemetry_data, 200 except ValueError as e: Loading
src/main.py +2 −4 Original line number Diff line number Diff line Loading @@ -23,7 +23,6 @@ from src.database.store_data import store_data from src.database.service_db import delete_data from src.mapper.main import mapper from src.realizer.main import realizer from src.planner.shortest_path import get_shortest_path from src.realizer.send_controller import send_controller class NSController: Loading Loading @@ -197,7 +196,7 @@ class NSController: "setup_time": setup_time } def monitoring(self, slice_id, slo_sle_template, sdps): def monitoring(self, slice_id, slo_sle_template): """ Monitor the status of a specific network slice. Loading @@ -219,8 +218,7 @@ class NSController: """ payload = { "slice_id": slice_id, "slo_sle_template": slo_sle_template, "sdps": sdps "slo_sle_template": slo_sle_template } # Request the realizer to retrieve the metrics for the links of the specified slice Loading
src/planner/shortest_path.py +2 −2 Original line number Diff line number Diff line Loading @@ -35,7 +35,7 @@ def get_shortest_path(network, src_node_id, dst_node_id, directed_graph: bool = nodes = network["node"] links = network["ietf-network-topology:link"] # Construir grafo # Build graph graph = { normalize_node_id(node["node-id"]): set() for node in nodes Loading Loading @@ -77,7 +77,7 @@ def get_shortest_path(network, src_node_id, dst_node_id, directed_graph: bool = if not visited[destination_node_idx]: return {"message": "No path found"}, 404 # Reconstruir camino # Rebuild path path = [] at = destination_node_idx Loading
src/realizer/main.py +10 −16 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ from .select_way import select_way from .nrp_handler import nrp_handler from .get_metrics import get_metrics from src.utils.safe_get import safe_get from src.planner.shortest_path import get_shortest_path from src.database.service_db import get_data_by_slice_id from src.realizer.restconf.connectors.tfs_connector import tfs_connector from flask import current_app Loading Loading @@ -90,20 +90,14 @@ def realizer(payload, need_nrp=False, order=None, nrp=None, controller_type=None elif action == "MONITOR": logging.debug("Realizer action: MONITOR") slice_id = payload.get("slice_id", None) sdps = payload.get("sdps", None) sdps_object = safe_get(sdps, ['network-slice-services', 'slice-service']) sdps_object = sdps_object[slice_id]['sdps']['sdp'] sdp_ids = [sdp["id"] for sdp in sdps_object] # Retrieve Network topology topology, response = tfs_connector().get_network_topology(current_app.config["RESTCONF_IP"],slice_id) service_data = get_data_by_slice_id(slice_id) if len(service_data) > 1: raise ValueError(f"Slices with multiple associated services are not supported: '{slice_id}'") service_id = service_data[0]["service_id"] # Retrieve Service Path path, response = tfs_connector().get_service_path(current_app.config["RESTCONF_IP"], service_id) if response == 200: logging.debug(f"Retrieved topology for slice '{slice_id}'") # Get shortest path path, response = get_shortest_path(topology, sdp_ids[0], sdp_ids[1]) if response == 200: logging.debug(f"Retrieved shortest path for slice '{slice_id}': {path}") logging.debug(f"Retrieved service path for slice '{slice_id}' (service '{service_id}'): {path}") get_metrics(path, slice_id, controller_type) else: raise Exception("Error: Shortest path not retrieved ") else: raise Exception("Error: Topology not retrieved") No newline at end of file raise Exception("Error: Service path not retrieved") No newline at end of file
src/realizer/restconf/connectors/tfs_connector.py +65 −1 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import logging, requests, json, aiohttp, asyncio, threading from src.config.constants import NBI_L2_PATH, NBI_L3_PATH, NBI_IETF_NETWORKS_PATH, NBI_SIMAP_SUSCRIPTION_PATH from typing import Dict, Tuple from typing import Dict, Tuple, List # Temp until moving to .env SDN_SUBSCRIPTION_PERIOD = 10 # seconds Loading Loading @@ -141,6 +141,70 @@ class tfs_connector(): return network, 200 def get_device_name(self, tfs_ip: str, device_uuid: str) -> str: """ Get device name for a given device_uuid from TFS. Args: tfs_ip (str): IP address of the TFS instance device_uuid (str): Device UUID Returns: str: Name of the device """ url = f"http://{tfs_ip}/tfs-api/device/{device_uuid}" headers = {'accept': 'application/json'} response = requests.get(url, headers=headers, timeout=60) response.raise_for_status() device_data = response.json() return device_data.get("name", "") def get_service_path(self, tfs_ip: str, service_id: str) -> Tuple[List[str], int]: """ Get ordered list of node names along the service path from TFS. Args: tfs_ip (str): IP address of the TFS instance service_id (str): Service UUID Returns: Tuple[List[str], int]: List of node names in order and status code """ url = f'http://{tfs_ip}/tfs-api/context/admin/service/{service_id}/connections' headers = {'accept': 'application/json'} try: response = requests.get(url, headers=headers, timeout=60) if response.status_code == 200: data = response.json() connections = data.get("connections", []) raw_uuids = [] for conn in connections: for hop in conn.get("path_hops_endpoint_ids", []): device_id = hop.get("device_id", {}).get("device_uuid", {}).get("uuid") if device_id and (not raw_uuids or raw_uuids[-1] != device_id): raw_uuids.append(device_id) name_cache = {} path = [] for dev_uuid in raw_uuids: if dev_uuid not in name_cache: try: dev_name = self.get_device_name(tfs_ip, dev_uuid) name_cache[dev_uuid] = dev_name if dev_name else dev_uuid except Exception as e: logging.warning(f"Could not retrieve device name for '{dev_uuid}': {e}") name_cache[dev_uuid] = dev_uuid path.append(name_cache[dev_uuid]) logging.debug(f"Retrieved service path for service '{service_id}': {path}") return path, 200 else: logging.error(f"Failed to retrieve service path for service '{service_id}': status {response.status_code}") return [], response.status_code except Exception as e: logging.exception(f"Error retrieving service path for service '{service_id}': {e}") return [], 500 # --- SDN STREAMS --- async def get_session(self): Loading