From 4f09887835a77fc6f59233e000b7d59700598d9c Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:23 +0200 Subject: [PATCH 01/27] Monitor System v1.0.0 --- PM Collections/NSC.postman_collection.json | 448 +++++++++++++++++ PM Collections/NSC.postman_environment.json | 28 ++ app.py | 3 +- requirements.txt | 4 +- src/api/main.py | 517 +++++++++++++++++++- swagger/restconf_namespace.py | 152 +++++- 6 files changed, 1145 insertions(+), 7 deletions(-) create mode 100644 PM Collections/NSC.postman_collection.json create mode 100644 PM Collections/NSC.postman_environment.json diff --git a/PM Collections/NSC.postman_collection.json b/PM Collections/NSC.postman_collection.json new file mode 100644 index 0000000..08f9531 --- /dev/null +++ b/PM Collections/NSC.postman_collection.json @@ -0,0 +1,448 @@ +{ + "info": { + "_postman_id": "43660461-a738-488b-a6f5-2c244a9cae25", + "name": "NSC", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "45361142", + "_collection_link": "https://go.postman.co/collection/45361142-43660461-a738-488b-a6f5-2c244a9cae25?source=collection_link" + }, + "item": [ + { + "name": "Operations", + "item": [ + { + "name": "Clients", + "item": [ + { + "name": "Create Client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Get All Clients", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients" + ] + } + }, + "response": [] + }, + { + "name": "Delete Client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Subscriptions", + "item": [ + { + "name": "Slices", + "item": [ + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"frequency\": 2\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "All subscriptions of a client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "All subscriptions of a client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Stream", + "item": [ + { + "name": "Stream All Telemetry Subscriptions by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "stream", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Stream Telemetry Subscription by client Copy", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "stream", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Telemetry of a Slice", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/PM Collections/NSC.postman_environment.json b/PM Collections/NSC.postman_environment.json new file mode 100644 index 0000000..19c9e3b --- /dev/null +++ b/PM Collections/NSC.postman_environment.json @@ -0,0 +1,28 @@ +{ + "id": "fb33daed-2c4b-4c85-a9b2-4d338123ed0b", + "name": "NSC", + "values": [ + { + "key": "HOST", + "value": "http://172.17.0.2:8086/nsc", + "type": "default", + "enabled": true + }, + { + "key": "CLIENT_ID", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "SLICE_ID", + "value": "", + "type": "default", + "enabled": true + } + ], + "color": null, + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-03-27T10:00:42.609Z", + "_postman_exported_using": "Postman/12.3.5" +} \ No newline at end of file diff --git a/app.py b/app.py index b6e06ce..d036ce7 100644 --- a/app.py +++ b/app.py @@ -58,6 +58,7 @@ def create_app(): init_db() app = Flask(__name__) app = create_config(app) + app.logger.setLevel(logging.INFO) CORS(app) # Configure logging to provide clear and informative log messages @@ -102,4 +103,4 @@ def create_app(): if __name__ == "__main__": app = create_app() - app.run(host="0.0.0.0", port=NSC_PORT, debug=True) \ No newline at end of file + app.run(host="0.0.0.0", port=NSC_PORT, debug=True, threaded=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 863e3ab..555cf5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -Flask +flask[async] flask-cors flask-restx netmiko @@ -25,4 +25,4 @@ coverage pytest libyang==3.3.0 sysrepo==1.7.6 - +aiohttp \ No newline at end of file diff --git a/src/api/main.py b/src/api/main.py index fe4849b..20ae5b9 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -14,6 +14,11 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. +import json +import time +from typing import Dict, List, Tuple +import uuid + from src.utils.send_response import send_response import logging from flask import current_app @@ -21,10 +26,26 @@ from src.database.db import get_data, delete_data, get_all_data, delete_all_data from src.realizer.tfs.helpers.tfs_connector import tfs_connector 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 collections import deque +import traceback + +import asyncio +import aiohttp class Api: + NSC_BASE_URL = "http://192.168.27.165" + SDN_TOPOLOGY_URL = f"{NSC_BASE_URL}/restconf/data/ietf-network:networks" + SDN_SUBSCRIBE_URL = f"{NSC_BASE_URL}/restconf/operations/subscriptions:establish-subscription" + SDN_SUBSCRIPTION_PERIOD = 10 # seconds + SDN_SUBS_INACTIVITY_THRESHOLD = 30 # seconds + def __init__(self, slice_service): self.slice_service = slice_service + if "slices" not in current_app.config: current_app.config["slices"] = {} + if "telemetryClients" not in current_app.config: current_app.config["telemetryClients"] = {} + if "slices" not in current_app.config: current_app.config["slices"] = {} + self.telemetry_cache = {} + def add_flow(self, intent): """ @@ -302,6 +323,11 @@ class Api: result = self.slice_service.nsc(intent) if result: create_data_store(intent) + for slice in result["slices"]: + current_app.config["slices"][slice["id"]] = { + "src": slice["source"], + "dst": slice["destination"] + } logging.info(f"Network Slice created successfully") return send_response( True, @@ -623,6 +649,7 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -675,6 +702,7 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].pop(slice_id, None) logging.info(f"Slice {slice_id} removed successfully") return {}, 204 @@ -693,6 +721,7 @@ class Api: if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -731,4 +760,490 @@ class Api: except ValueError as e: return send_response(False, code=404, message=str(e)) except Exception as e: - return send_response(False, code=500, message=str(e)) \ No newline at end of file + return send_response(False, code=500, message=str(e)) + + def getShortestPath(self, network, srcNodeId, dstNodeId, directedGraph: bool = False) -> Tuple[Dict[str, any], int]: + try: + sourceNodeIdx = srcNodeId + destNodeIdx = dstNodeId + nodes = network["node"] + links = network["ietf-network-topology:link"] + + # Construir grafo (no dirigido) + graph = {node["node-id"]: set() for node in nodes} + for link in links: + src = link["source"]["source-node"] + dst = link["destination"]["dest-node"] + + graph[src].add(dst) + if not directedGraph: graph[dst].add(src) + + # BFS + visited = { node["node-id"]: False for node in nodes } + prev = { node["node-id"]: -1 for node in nodes } + + queue = deque([sourceNodeIdx]) + visited[sourceNodeIdx] = True + + while queue: + current = queue.popleft() + if current == destNodeIdx: break + + for neighbor in graph[current]: + if not visited[neighbor]: + visited[neighbor] = True + prev[neighbor] = current + queue.append(neighbor) + + # Reconstruir camino + path = [] + at = destNodeIdx + if not visited[destNodeIdx]: return {"message": "No path found"}, 404 + + while at != -1: + path.append(at) + at = prev[at] + path.reverse() + return path, 200 + except Exception as e: + logging.info(f"Error calculating shortest path: {traceback.print_exc()}") + return {"message": str(e)}, 500 + + # --- SDN STREAMS --- + + async def listenStream(self, session, link_id, slice_id, stream_url): + async with session.get(stream_url) as resp: + buffer = "" + + logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") + async for chunk in resp.content: + buffer += chunk.decode() + + try: + # intenta parsear un JSON completo + data, index = json.JSONDecoder().raw_decode(buffer) + logging.info(f"Received telemetry for link {link_id} in slice {slice_id}: {data}") + buffer = buffer[index:].lstrip() + + telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + + self.telemetry_cache.setdefault(slice_id, {})[link_id] = { + "timestamp": data["notification"]["eventTime"], #time.time(), + "bandwidth": float(telemetry.get("bandwidth")), + "latency": float(telemetry.get("latency")), + "services": telemetry.get("services", []) + } + buffer = "" + except json.JSONDecodeError: pass # aún no hay suficiente data + logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") + + async def startStreams(self, links, slice_id): + async with aiohttp.ClientSession(timeout = aiohttp.ClientTimeout(total=10)) as session: + tasks = [] + + for link in links: + sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ + "ietf-subscribed-notifications:input": { + "datastore": "operational", + "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", + "ietf-yang-push:periodic": { + "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD + } + } + }, + auth=aiohttp.BasicAuth("admin", "admin") + ) + sub_data = await sub_resp.json() + logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") + """ + response format is: + { + "id": 1161739417341820929, + "uri": "/restconf/stream/1161739417341820929" + } + """ + + if "uri" in sub_data: + tasks.append( + asyncio.create_task( + self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) + ) + ) + await asyncio.gather(*tasks) + + # --- CLIENTS --- + + def createClient(self, clientId): + response, code = self.getClient(clientId) + if code == 400: return response, code + if code == 200: return {"message": f"Client '{clientId}' already exists"}, 400 + + current_app.config["telemetryClients"][clientId] = { "subscriptions": {} } + return {"message": f"Client '{clientId}' created successfully"}, 200 + + def deleteClient(self, clientId): + response, code = self.getClient(clientId) + if response != 200: return response, code + + del current_app.config["telemetryClients"][clientId] + return {"message": f"Client '{clientId}' deleted successfully"}, 200 + + def getClient(self, clientId) -> Tuple[Dict[str,any], int]: + if not clientId: return {"message": "clientId is required"}, 400 + clients = current_app.config["telemetryClients"] + + if clientId not in clients: return {"message": f"Client '{clientId}' not found"}, 404 + return clients[clientId], 200 + + def getAllClients(self) -> Tuple[Dict[str,any], int]: return current_app.config["telemetryClients"], 200 + + # --- TELEMETRY --- + + async def getNetworkTopology(self, sliceId) -> Tuple[Dict[str, any], int]: + # MOCKED TOPOLOGY + #topology = requests.get(Api.SDN_TOPOLOGY_URL) + topology = [ + { + "ietf-network:networks": { + "network": [ + { + "ietf-network-topology:link": [ + { + "destination": { + "dest-node": "AMF-N2", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "4.4.4.4-AMF-N2", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/2-GigabitEthernet0/0/0/2" + } + }, + { + "destination": { + "dest-node": "1.1.1.1", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "4.4.4.4-1.1.1.1", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "3.3.3.3", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "CU-N2-3.3.3.3", + "source": { + "source-node": "CU-N2", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "1.1.1.1-4.4.4.4", + "source": { + "source-node": "1.1.1.1", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "CU-N2", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "1.1.1.1-CU-N2", + "source": { + "source-node": "1.1.1.1", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/2-GigabitEthernet0/0/0/2" + }, + "link-id": "AMF-N2-4.4.4.4", + "source": { + "source-node": "AMF-N2", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "CU-N2", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "3.3.3.3-CU-N2", + "source": { + "source-node": "3.3.3.3", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "3.3.3.3-4.4.4.4", + "source": { + "source-node": "3.3.3.3", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "3.3.3.3", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "4.4.4.4-3.3.3.3", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "1.1.1.1", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "CU-N2-1.1.1.1", + "source": { + "source-node": "CU-N2", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + } + ], + "network-id": "slice_1 MOCKED", + "network-types": { + "ietf-l3-unicast-topology:l3-unicast-topology": {} + }, + "node": [ + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "CU-N2" + }, + "node-id": "CU-N2" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "AMF-N2" + }, + "node-id": "AMF-N2" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "1.1.1.1" + }, + "node-id": "1.1.1.1" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "4.4.4.4" + }, + "node-id": "4.4.4.4" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "3.3.3.3" + }, + "node-id": "3.3.3.3" + } + ] + } + ] + } + } + ] if True else (await response.json()) + + network = topology[0]["ietf-network:networks"]["network"] + network = network[0] #next((n for n in network if n["network-id"] == sliceId), None) + return network, 200 + + async def getTelemetry(self, sliceId, subscribe: bool = False) -> Dict[str, any]: + if sliceId in current_app.config["slices"]: + #Retrieve Network topology: + config = current_app.config["slices"][sliceId] + if "topology" not in config or config["topology"] is None: + topology, code = await self.getNetworkTopology(sliceId) #Currently mocked + if code != 200: return { "message": f"There was an error retrieving topology for slice '{sliceId}': {topology}" } + config["topology"] = topology + else: topology = config["topology"] + + #Get shortest path from source node to destination node: + if "shortestPath" not in config or config["shortestPath"] is None: + shortestPath, code = self.getShortestPath(topology, config["src"], config["dst"]) + logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + if code != 200: return shortestPath + config["shortestPath"] = shortestPath + else: shortestPath = config["shortestPath"] + logging.info(f"[getTelemetry] Shortest path for slice '{sliceId}': {shortestPath}") + + # Make sure data is already initialized in cache + # (this will only happen if an already-opened stream with SDN controller has sent data for this slice, + # otherwise it will be initialized when the first data arrives): + if sliceId in self.telemetry_cache: + """ the format is the following: + self.telemetry_cache[sliceId][linkId] = { + "timestamp": str, #time.time(), + "bandwidth": float, + "latency": float, + "services": List[any] + } + """ + return { + "sliceId": sliceId, + "telemetry": { + "desc": f"Aggregated telemetry of a Network slice {sliceId}. Other additional fields should be included in the real version.", + "latency": f"{sum([link.latency for key, link in self.telemetry_cache[sliceId].items()])}ms", + "bandwidth": f"{sum([link.bandwidth for key, link in self.telemetry_cache[sliceId].items()])}Mb/s" + }, + "timestamp": time.time() + } + else: + if subscribe: # Open a stream with SDN controller for this slice: + # Get telemetry of every node in the slice: + """ + self.startStreams(self, [ + link["link-id"] + for link in topology["ietf-network-topology:link"] + ], sliceId) + """ + + # Get telemetry only of the links in the shortest path: + await self.startStreams([ f"{shortestPath[i]}-{shortestPath[i+1]}" for i in range(len(shortestPath)-1) ], sliceId) + return { "message": f"No telemetry available for slice '{sliceId}'" } + else: return { "message": f"There is no slice with id '{sliceId}' registered" } + + async def getAllTelemetry(self, subscribe: bool = False) -> Dict[str, Dict[str, any]]: + telemetryData = {} + for sliceId in current_app.config.get("slices", {}).keys(): telemetryData[sliceId] = await self.getTelemetry(sliceId, subscribe) + return telemetryData + + # --- SUBSCRIPTIONS --- + + async def getSubscriptionJson(self, response, sliceId, subscribe: bool = False): + return { + "subscriptionFrequency": response["frequency"], + "sourceNode": current_app.config["slices"][sliceId]["src"] if sliceId in current_app.config["slices"] else "UNKNOWN", + "destinationNode": current_app.config["slices"][sliceId]["dst"] if sliceId in current_app.config["slices"] else "UNKNOWN", + "telemetry": await self.getTelemetry(sliceId, subscribe) + } + + async def getTelemetrySubscriptions(self, clientId, sliceId = None, subscribe: bool = False) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + if sliceId is not None: + if sliceId not in response["subscriptions"]: return {"message": f"Client '{clientId}' has no subscription for slice '{sliceId}'"}, 400 + + return { + "clientId": clientId, + "sliceSubscription": await self.getSubscriptionJson(response["subscriptions"][sliceId], sliceId, subscribe) + }, 200 + else: + return { + "clientId": clientId, + "sliceSubscriptions": [ (await self.getSubscriptionJson(sub, sId, subscribe)) for sId, sub in response["subscriptions"].items() ] + }, 200 + + async def createTelemetrySubscription(self, clientId, sliceId, frequency, src, dst) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + sliceExists = sliceId in response["subscriptions"] + + if not frequency: return {"message": "Field 'frequency' is required"}, 400 + + topology, code = await self.getNetworkTopology(sliceId) #Currently mocked + if code != 200: + msg = f"There was an error retrieving topology for slice '{sliceId}': {topology}" + logging.error(msg) + return { "message": msg }, code + + #Get shortest path from source node to destination node: + shortestPath, code = self.getShortestPath(topology, src, dst) + if code != 200: + logging.info(f"Error while computing shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + return shortestPath, code + else: + logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + + response["subscriptions"][sliceId] = { "frequency": frequency } + current_app.config["slices"][sliceId] = { + "src": src, + "dst": dst, + "topology": topology, + "shortestPath": shortestPath + } + + return { + "message": f"Subscription successfully {"modified" if sliceExists else "created"}", + "sliceId": sliceId, + "frequency": frequency #"clientId": str(uuid.uuid4()) + }, 201 + + def deleteTelemetrySubscriptions(self, clientId, sliceId = None) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + if sliceId: + if sliceId not in response["subscriptions"]: return {"message": f"Subscription '{sliceId}' not found for client {clientId}"}, 404 + + del response["subscriptions"][sliceId] + return {"message": "Subscription deleted"}, 200 + else: + response["subscriptions"] = {} + return {"message": "All subscriptions deleted"}, 200 + + def sync_stream(self, async_gen_func, *args, **kwargs): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + agen = async_gen_func(*args, **kwargs) + + try: + while True: yield loop.run_until_complete(agen.__anext__()) + except StopAsyncIteration: pass + finally: loop.close() + + async def openSubscriptions(self, clientId, sliceId = None): + try: + response, code = self.getClient(clientId) + if code != 200: yield f"event: error\ndata: {json.dumps(data)}\n\n" + else: + frequency = 0 + okToProceed = True + # Process only for one slice: + if sliceId: + if sliceId in response["subscriptions"]: + frequency = response["subscriptions"][sliceId]["frequency"] + yield f"data: Using frequency of {frequency}...\n\n" + else: + yield f"data: sliceId '{sliceId}' is not present within '{clientId}' subscriptions. Closing connection now...\n\n" + okToProceed = False + # Process for all slices: + else: + # Using policy of highest frequency among the subscribed slices, but other policies could be implemented (e.g., average/min frequency, one stream per slice with its own frequency, etc.) + frequency = max([ sub["frequency"] for _, sub in response["subscriptions"].items() ]) if response["subscriptions"] else 0 + #for sliceId, sub in response["subscriptions"].items(): frequency = max(frequency, sub["frequency"]) + yield f"data: Using frequency of {frequency}...\n\n" + + while okToProceed: + try: + data, code = await self.getTelemetrySubscriptions(clientId, sliceId, True) + if code == 200: + yield f"data: {json.dumps(data)}\n\n" + await asyncio.sleep(frequency) + else: + yield f"event: error\ndata: {json.dumps(data)}\n\n" + break + except aiohttp.ClientError as e: + errorStr = "Connection timeout to SDN Controller." if "Connection timeout to host" in str(e) else str(e) + yield f"event: error\ndata: {json.dumps({'error': errorStr})}\n\n" + break + except GeneratorExit: + current_app.logger.info(traceback.print_exc()) + #current_app.config["telemetryClients"].pop(clientId, None) + except Exception: current_app.logger.info(traceback.print_exc()) \ No newline at end of file diff --git a/swagger/restconf_namespace.py b/swagger/restconf_namespace.py index 40c4c9f..bd758b5 100644 --- a/swagger/restconf_namespace.py +++ b/swagger/restconf_namespace.py @@ -14,8 +14,13 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -from flask import request -from flask_restx import Namespace, Resource, reqparse +import logging +import time +import json +import uuid + +from flask import request, Response, request, current_app, stream_with_context +from flask_restx import Namespace, Resource, fields, reqparse from src.main import NSController from src.api.main import Api from swagger.models.create_models_restconf import create_ietf_network_slice_nbi_yang_model @@ -211,4 +216,145 @@ class Sdp(Resource): @restconf_ns.response(204, "SDP deleted") def delete(self, slice_service_id, sdp_id): controller = NSController(controller_type="RESTCONF") - return Api(controller).delete_sdps(slice_service_id, sdp_id) \ No newline at end of file + return Api(controller).delete_sdps(slice_service_id, sdp_id) + +# ================================================================================================================================================ +@restconf_ns.route("/telemetry/operations/clients") +class TelemetryAllClientsRequest(Resource): + @restconf_ns.doc(description="Gets the list of all registered clients") + @restconf_ns.response(200, "Clients retrieved") + def get(self): + clients, code = Api(NSController(controller_type="RESTCONF")).getAllClients() + return [clientName for clientName, clientData in clients.items()], code + +@restconf_ns.route("/telemetry/operations/clients/") +class TelemetryClientRequest(Resource): + @restconf_ns.doc(description="Creates a new client") + @restconf_ns.response(200, "Client created") + def post(self, clientId): return Api(NSController(controller_type="RESTCONF")).createClient(clientId) + + @restconf_ns.doc(description="Deletes a given client") + @restconf_ns.response(200, "Client deleted") + def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteClient(clientId) + + +@restconf_ns.route("/telemetry/operations/slices") +class TelemetrySliceRequest(Resource): + @restconf_ns.doc(description="Gets latest telemetry of a certain network slice, or of all slices.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, retrieves all slices.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(200, "Telemetry retrieved") + @restconf_ns.response(404, "Slice not found") + def get(self): + api = Api(NSController(controller_type="RESTCONF")) + sliceId = request.args.get("sliceId", None) + + ret = {} + if sliceId: ret = current_app.ensure_sync(api.getTelemetry)(sliceId) + else: ret = current_app.ensure_sync(api.getAllTelemetry)() + + logging.info(f"Telemetry retrieved successfully for sliceId={sliceId if sliceId else 'ALL'}: {ret}") + return ret, 200 + +@restconf_ns.route("/telemetry/operations/stream/") +class TelemetryStreamRequest(Resource): + @restconf_ns.doc( + description="Opens a telemetry stream of a given subscription (or all of them) of a certain client", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, streams all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Telemetry opened") + @restconf_ns.response(400, "Invalid request") + def get(self, clientId): + api = Api(NSController(controller_type="RESTCONF")) + return Response( + stream_with_context( + api.sync_stream( + api.openSubscriptions, + clientId, + request.args.get("sliceId", None) + ) + ), + mimetype="text/event-stream" + ) + + +@restconf_ns.route("/telemetry/operations/slice-subscriptions/") +class TelemetrySubscriptionsRequest(Resource): + @restconf_ns.doc( + description="Retrieves a specific telemetry subscription (or all of them) of a given client.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, retrieves all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription(s) successfully retrieved") + @restconf_ns.response(400, "Invalid request") + def get(self, clientId): + api = Api(NSController(controller_type="RESTCONF")) + return current_app.ensure_sync(api.getTelemetrySubscriptions)(clientId, request.args.get("sliceId", None), False) + + @restconf_ns.doc( + description="Deletes a specific telemetry subscription (or all of them) of a given client.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, deletes all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription(s) successfully deleted") + @restconf_ns.response(400, "Invalid request") + def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteTelemetrySubscriptions(clientId, request.args.get("sliceId", None)) + + @restconf_ns.doc( + description="Creates a new telemetry subscription for a given client.", + params={ + "sliceId": { + "description": "Network Slice ID. If not provided, the sliceId must be included in the request body.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription successfully created") + @restconf_ns.response(400, "Invalid request") + @restconf_ns.expect(restconf_ns.model("TelemetrySubscriptionModel", { + "sliceId": fields.String(required=False, description="Network Slice ID (if not provided as query param)"), + "frequency": fields.Integer(required=True, description="Telemetry push frequency in seconds"), + "src": fields.String(required=True, description="Source Node Id"), + "dst": fields.String(required=True, description="Destination Node Id") + }), validate=True) + def post(self, clientId): + sliceId = request.args.get("sliceId", None) + if not sliceId: sliceId = request.json.get("sliceId", None) + if not sliceId: return {"error": "sliceId query param is required"}, 400 + + api = Api(NSController(controller_type="RESTCONF")) + return current_app.ensure_sync(api.createTelemetrySubscription)( + clientId, + sliceId, + request.json.get("frequency", None), + request.json.get("src", None), + request.json.get("dst", None), + ) \ No newline at end of file -- GitLab From 7bfdac4fdffbdfed9d54c5deacc280a3d263c7e6 Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:34 +0200 Subject: [PATCH 02/27] NSC Monitor system v1.0.0 --- src/api/main.py | 169 ++++++++++++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 62 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 20ae5b9..7c51c21 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -19,6 +19,8 @@ import time from typing import Dict, List, Tuple import uuid +import requests + from src.utils.send_response import send_response import logging from flask import current_app @@ -33,7 +35,7 @@ import asyncio import aiohttp class Api: - NSC_BASE_URL = "http://192.168.27.165" + NSC_BASE_URL = "http://192.168.27.189" SDN_TOPOLOGY_URL = f"{NSC_BASE_URL}/restconf/data/ietf-network:networks" SDN_SUBSCRIBE_URL = f"{NSC_BASE_URL}/restconf/operations/subscriptions:establish-subscription" SDN_SUBSCRIPTION_PERIOD = 10 # seconds @@ -45,8 +47,17 @@ class Api: if "telemetryClients" not in current_app.config: current_app.config["telemetryClients"] = {} if "slices" not in current_app.config: current_app.config["slices"] = {} self.telemetry_cache = {} + self.session = None #For connections with SDN - + async def get_session(self): + # Crea la sesión si no existe + if self.session is None or self.session.closed: self.session = aiohttp.ClientSession() + return self.session + + async def close_session(self): + # Llama a esto al apagar tu app + if self.session: await self.session.close() + def add_flow(self, intent): """ Create a new transport network slice. @@ -812,64 +823,82 @@ class Api: # --- SDN STREAMS --- async def listenStream(self, session, link_id, slice_id, stream_url): - async with session.get(stream_url) as resp: - buffer = "" - - logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") - async for chunk in resp.content: - buffer += chunk.decode() - - try: - # intenta parsear un JSON completo - data, index = json.JSONDecoder().raw_decode(buffer) - logging.info(f"Received telemetry for link {link_id} in slice {slice_id}: {data}") - buffer = buffer[index:].lstrip() - - telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] - - self.telemetry_cache.setdefault(slice_id, {})[link_id] = { - "timestamp": data["notification"]["eventTime"], #time.time(), - "bandwidth": float(telemetry.get("bandwidth")), - "latency": float(telemetry.get("latency")), - "services": telemetry.get("services", []) - } - buffer = "" - except json.JSONDecodeError: pass # aún no hay suficiente data + try: + async with session.get( + stream_url, + auth=aiohttp.BasicAuth("admin", "admin"), + timeout=None, + headers = { + "Accept": "text/event-stream", + "Cache-Control": "no-cache" + } + ) as resp: + buffer = "" + + logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") + + async for chunk in resp.content.iter_any(): + buffer += chunk.decode() + + # SSE separa los eventos con dos saltos de línea (\n\n) + while "\n\n" in buffer: + # Extraemos el evento completo y dejamos el resto en el buffer + event_block, buffer = buffer.split("\n\n", 1) + + for line in event_block.split("\n"): + if line.startswith("data:"): + json_str = line.replace("data:", "").strip() + + try: + data = json.loads(json_str) + logging.info(f"JSON detected: {data}") + + telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + self.telemetry_cache.setdefault(slice_id, {})[link_id] = { + "timestamp": data["notification"]["eventTime"], + "bandwidth": float(telemetry.get("bandwidth-utilization", 0)), + "latency": float(telemetry.get("latency", 0)), + "services": telemetry.get("related-service-ids", []) + } + except json.JSONDecodeError: logging.error("Error parsing the JSON from field 'data'") + except Exception as e: logging.error(f"Error processing logic: {e}") + except aiohttp.ClientError as e: logging.error(f"Connection error with stream for link {link_id}: {e}") logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") async def startStreams(self, links, slice_id): - async with aiohttp.ClientSession(timeout = aiohttp.ClientTimeout(total=10)) as session: - tasks = [] - - for link in links: - sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ - "ietf-subscribed-notifications:input": { - "datastore": "operational", - "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", - "ietf-yang-push:periodic": { - "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD - } + #async with self.get_session() as session: #timeout = aiohttp.ClientTimeout(total=10) + session = await self.get_session() + tasks = [] + + for link in links: + sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ + "ietf-subscribed-notifications:input": { + "datastore": "operational", + "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", + "ietf-yang-push:periodic": { + "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD } - }, - auth=aiohttp.BasicAuth("admin", "admin") - ) - sub_data = await sub_resp.json() - logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") - """ - response format is: - { - "id": 1161739417341820929, - "uri": "/restconf/stream/1161739417341820929" } - """ - - if "uri" in sub_data: - tasks.append( - asyncio.create_task( - self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) - ) + }, + auth=aiohttp.BasicAuth("admin", "admin") + ) + sub_data = await sub_resp.json() + logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") + """ + response format is: + { + "id": 1161739417341820929, + "uri": "/restconf/stream/1161739417341820929" + } + """ + + if "uri" in sub_data: + tasks.append( + asyncio.create_task( + self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) ) - await asyncio.gather(*tasks) + ) + #await asyncio.gather(*tasks) # --- CLIENTS --- @@ -901,7 +930,10 @@ class Api: async def getNetworkTopology(self, sliceId) -> Tuple[Dict[str, any], int]: # MOCKED TOPOLOGY - #topology = requests.get(Api.SDN_TOPOLOGY_URL) + topology = requests.get(Api.SDN_TOPOLOGY_URL, auth=("admin", "admin")) + networkRaw = topology.json() + + """ topology = [ { "ietf-network:networks": { @@ -1060,9 +1092,11 @@ class Api: } } ] if True else (await response.json()) + #""" + + network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == sliceId), None) + logging.info(f"Retrieved topology for slice '{sliceId}': {network}") - network = topology[0]["ietf-network:networks"]["network"] - network = network[0] #next((n for n in network if n["network-id"] == sliceId), None) return network, 200 async def getTelemetry(self, sliceId, subscribe: bool = False) -> Dict[str, any]: @@ -1082,7 +1116,6 @@ class Api: if code != 200: return shortestPath config["shortestPath"] = shortestPath else: shortestPath = config["shortestPath"] - logging.info(f"[getTelemetry] Shortest path for slice '{sliceId}': {shortestPath}") # Make sure data is already initialized in cache # (this will only happen if an already-opened stream with SDN controller has sent data for this slice, @@ -1099,14 +1132,26 @@ class Api: return { "sliceId": sliceId, "telemetry": { - "desc": f"Aggregated telemetry of a Network slice {sliceId}. Other additional fields should be included in the real version.", - "latency": f"{sum([link.latency for key, link in self.telemetry_cache[sliceId].items()])}ms", - "bandwidth": f"{sum([link.bandwidth for key, link in self.telemetry_cache[sliceId].items()])}Mb/s" + "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields should be included in the real version.", + "latency": f"{sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()])}ms", + "bandwidth": f"{sum([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()])}Mb/s" }, "timestamp": time.time() } else: if subscribe: # Open a stream with SDN controller for this slice: + shortestPathLinks = [] + if sliceId not in self.telemetry_cache: self.telemetry_cache[sliceId] = {} + for i in range(len(shortestPath)-1): + linkId = f"{shortestPath[i]}-{shortestPath[i+1]}" + shortestPathLinks.append(linkId) + self.telemetry_cache[sliceId][linkId] = { + "timestamp": None, #time.time(), + "bandwidth": 0, + "latency": 0, + "services": [] + } + # Get telemetry of every node in the slice: """ self.startStreams(self, [ @@ -1116,7 +1161,7 @@ class Api: """ # Get telemetry only of the links in the shortest path: - await self.startStreams([ f"{shortestPath[i]}-{shortestPath[i+1]}" for i in range(len(shortestPath)-1) ], sliceId) + await self.startStreams(shortestPathLinks, sliceId) return { "message": f"No telemetry available for slice '{sliceId}'" } else: return { "message": f"There is no slice with id '{sliceId}' registered" } -- GitLab From 21d2fc1f594405e3080d9072b8c922fbd34c46de Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:39 +0200 Subject: [PATCH 03/27] Add GAIA Architecture --- .DS_Store | Bin 0 -> 6148 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/api/main.py | 39 ++++++++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0387394593eb6a564c8dbd9de88228aceaa7c799 GIT binary patch literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLftjJ0p@<=mA&7utoqAWI#~@svK0^gR5djNE-sg2WcN|2*3hplpYO%0SEyAmZ=eR literal 0 HcmV?d00001 diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..45e350a5457c5e7e299d845881b71036b878945f GIT binary patch literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLfrY`H!I8m}!7(kRI5{alKL@Ie2}*<1 zFoMiLlZQ%=QllX-8UmvsFd71*Aut*OgD3<*^){&f=N?3E8gbS3Ltr!nhHD5gvbY4hIDsln96kiqxd~7? z5F1n#GlJ@5h#qiN3~K~1K?W2hpvpnjJ-8}ngtQ?*e316hh5#&pM(NQI7=RD}07=Rc A2><{9 literal 0 HcmV?d00001 diff --git a/src/api/main.py b/src/api/main.py index 7c51c21..236f019 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -334,10 +334,14 @@ class Api: result = self.slice_service.nsc(intent) if result: create_data_store(intent) + + slos = safe_get(intent, ['ietf-network-slice-service:network-slice-services', 'slo-sle-templates', 'slo-sle-template'], []) + slosDict = { slo["id"]: { "slo-policy": slo["slo-policy"], "sle-policy": slo["sle-policy"] } for slo in slos } for slice in result["slices"]: current_app.config["slices"][slice["id"]] = { "src": slice["source"], - "dst": slice["destination"] + "dst": slice["destination"], + "slo-sle-policy": slosDict.get(slice.get("slo-sle-template", ""), {}), } logging.info(f"Network Slice created successfully") return send_response( @@ -854,10 +858,12 @@ class Api: logging.info(f"JSON detected: {data}") telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + bw = telemetry.get("bandwidth-utilization", "N/A") + latency = telemetry.get("latency", "N/A") self.telemetry_cache.setdefault(slice_id, {})[link_id] = { "timestamp": data["notification"]["eventTime"], - "bandwidth": float(telemetry.get("bandwidth-utilization", 0)), - "latency": float(telemetry.get("latency", 0)), + "bandwidth": float(bw) if bw != "N/A" else bw, + "latency": float(latency) if latency != "N/A" else latency, "services": telemetry.get("related-service-ids", []) } except json.JSONDecodeError: logging.error("Error parsing the JSON from field 'data'") @@ -1094,7 +1100,7 @@ class Api: ] if True else (await response.json()) #""" - network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == sliceId), None) + network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == "admin"), None) logging.info(f"Retrieved topology for slice '{sliceId}': {network}") return network, 200 @@ -1129,13 +1135,24 @@ class Api: "services": List[any] } """ + + slo = current_app.config["slices"][sliceId]["slo-sle-policy"].get("slo-policy", {}) + sloBandwith = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-bandwidth" ][0] if slo else 0 + sloLatency = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-delay-maximum" ][0] if slo else "N/A" + + bw = min([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()]) + lt = sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()]) + + warningJson = { "warning": {} } + if sloLatency != "N/A" and lt > float(sloLatency): warningJson["warning"]["latency"] = f"High latency: {lt}ms exceeds SLO of {sloLatency}ms" + if sloBandwith != 0 and bw > float(sloBandwith): warningJson["warning"]["bandwidth"] = f"High bandwidth utilization: {bw}Kb/s exceeds SLO of {sloBandwith}Kb/s" return { "sliceId": sliceId, "telemetry": { - "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields should be included in the real version.", - "latency": f"{sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()])}ms", - "bandwidth": f"{sum([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()])}Mb/s" - }, + "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields could be included in the future.", + "latency": f"{lt}ms", + "bandwidth": f"{bw}Mb/s" + } + warningJson, "timestamp": time.time() } else: @@ -1278,8 +1295,12 @@ class Api: while okToProceed: try: data, code = await self.getTelemetrySubscriptions(clientId, sliceId, True) + if code == 200: - yield f"data: {json.dumps(data)}\n\n" + if "warning" in data["sliceSubscription"]["telemetry"]: + logging.warning(f"Warning for client '{clientId}' in slice '{sliceId}': {data['sliceSubscription']['telemetry']['warning']}") + yield f"event: warning\ndata: {json.dumps(data)}\n\n" + else: yield f"data: {json.dumps(data)}\n\n" await asyncio.sleep(frequency) else: yield f"event: error\ndata: {json.dumps(data)}\n\n" -- GitLab From e54ee24cbfd8d3a7da2aaf1cf9fdf7d19fc5bdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Tue, 5 May 2026 10:50:58 +0000 Subject: [PATCH 04/27] Update copyright headers --- .dockerignore | 14 + .gitignore | 14 + .gitlab-ci.yml | 2 +- Dockerfile | 4 +- app.py | 2 +- deploy.sh | 2 +- out-no-header.txt | 16 + out-skipped.txt | 381 ++++++++++++++++++ out-updated.txt | 88 ++++ requirements.txt | 2 +- scripts/dump_templates_nsc.sh | 2 +- scripts/show_logs_nsc.sh | 2 +- scripts/update_license_headers.py | 165 ++++++++ src/api/main.py | 2 +- src/config/.env.example | 14 +- src/config/config.py | 2 +- src/config/constants.py | 4 +- src/database/db.py | 2 +- src/database/store_data.py | 2 +- src/database/sysrepo_store.py | 2 +- src/main.py | 2 +- src/mapper/extract_sdp_info.py | 2 +- src/mapper/get_service_template.py | 2 +- src/mapper/get_template.py | 2 +- src/mapper/main.py | 2 +- src/mapper/process_connnectivity.py | 2 +- src/mapper/slo_viability.py | 2 +- src/nbi_processor/detect_format.py | 2 +- src/nbi_processor/main.py | 2 +- src/nbi_processor/translator.py | 2 +- src/planner/energy_planner/energy.py | 2 +- src/planner/hrat_planner/hrat.py | 2 +- src/planner/planner.py | 2 +- .../tfs_optical_planner/tfs_optical.py | 2 +- src/realizer/e2e/e2e_connect.py | 2 +- src/realizer/e2e/main.py | 2 +- .../e2e/service_types/del_l3ipowdm_slice.py | 2 +- .../e2e/service_types/l3ipowdm_slice.py | 2 +- src/realizer/ixia/helpers/NEII_V4.py | 2 +- .../ixia/helpers/automatizacion_ne2v4.py | 2 +- src/realizer/ixia/ixia_connect.py | 2 +- src/realizer/ixia/main.py | 2 +- src/realizer/main.py | 2 +- src/realizer/nrp_handler.py | 2 +- .../restconf/connectors/cisco_connector.py | 2 +- .../restconf/connectors/frr_connector.py | 2 +- .../restconf/connectors/tfs_connector.py | 2 +- src/realizer/restconf/main.py | 2 +- src/realizer/restconf/restconf_connect.py | 2 +- .../builders/apply_metric_constraint.py | 2 +- .../builders/configure_match_criteria.py | 2 +- .../service_types/builders/configure_slos.py | 10 +- .../builders/create_network_access.py | 14 +- .../builders/create_site_from_sdp.py | 5 +- .../builders/initialize_structure.py | 2 +- src/realizer/restconf/service_types/l2vpn.py | 2 +- src/realizer/restconf/service_types/l3vpn.py | 2 +- src/realizer/select_way.py | 2 +- src/realizer/send_controller.py | 4 +- src/realizer/tfs/helpers/cisco_connector.py | 2 +- src/realizer/tfs/helpers/tfs_connector.py | 2 +- src/realizer/tfs/main.py | 2 +- src/realizer/tfs/service_types/tfs_l2vpn.py | 2 +- src/realizer/tfs/service_types/tfs_l3vpn.py | 2 +- src/realizer/tfs/tfs_connect.py | 2 +- src/tests/test_api.py | 16 + src/tests/test_database.py | 16 + src/tests/test_e2e.py | 16 + src/tests/test_initialization.py | 16 + src/tests/test_mapper.py | 16 + src/tests/test_nbi_processor.py | 16 + src/tests/test_utils.py | 16 + src/utils/build_response.py | 2 +- src/utils/dump_templates.py | 2 +- src/utils/load_template.py | 2 +- src/utils/safe_get.py | 2 +- src/utils/send_response.py | 2 +- src/utils/slice_manager.py | 2 +- src/webui/gui.py | 2 +- src/webui/templates/dev.html | 2 +- src/webui/templates/index.html | 2 +- src/webui/templates/ixia.html | 2 +- src/webui/templates/login.html | 2 +- src/webui/templates/search.html | 2 +- src/webui/templates/welcome.html | 2 +- swagger/E2E_namespace.py | 2 +- swagger/ixia_namespace.py | 2 +- swagger/models/create_models.py | 2 +- swagger/models/create_models_restconf.py | 2 +- swagger/restconf_namespace.py | 2 +- swagger/tfs_namespace.py | 2 +- 91 files changed, 894 insertions(+), 93 deletions(-) create mode 100644 out-no-header.txt create mode 100644 out-skipped.txt create mode 100644 out-updated.txt create mode 100644 scripts/update_license_headers.py diff --git a/.dockerignore b/.dockerignore index b895f09..7943f8f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,17 @@ +# Copyright 2022-2026 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. + __pycache__/ *.pyc *.db diff --git a/.gitignore b/.gitignore index ef16b43..5207b65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +# Copyright 2022-2026 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. + __pycache__/ swagger/__pycache__/ src/__pycache__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a50341e..f8dcd65 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/Dockerfile b/Dockerfile index 0d65d4e..66bb17b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -75,7 +75,7 @@ RUN pip install --upgrade pip && \ COPY . /app # Expose port -EXPOSE 8086 +EXPOSE 8085 RUN sysrepoctl -i /app/yang-modules/ietf-routing-types@2017-12-04.yang \ -i /app/yang-modules/ietf-geo-location@2022-02-11.yang \ diff --git a/app.py b/app.py index d036ce7..c87f2c3 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/deploy.sh b/deploy.sh index 8bd99bf..c58d17d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/out-no-header.txt b/out-no-header.txt new file mode 100644 index 0000000..03c635a --- /dev/null +++ b/out-no-header.txt @@ -0,0 +1,16 @@ +./slice.db +./yang-modules/ietf-ac-common@2025-09-29.yang +./yang-modules/ietf-key-chain@2017-06-15.yang +./yang-modules/ietf-routing-types@2017-12-04.yang +./yang-modules/ietf-network@2018-02-26.yang +./yang-modules/ietf-ethertypes@2019-03-04.yang +./yang-modules/ietf-netconf-acm@2018-02-14.yang +./yang-modules/ietf-vpn-common@2022-02-11.yang +./yang-modules/ietf-te-types@2025-10-17.yang +./yang-modules/ietf-geo-location@2022-02-11.yang +./yang-modules/ietf-ac-svc@2025-09-29.yang +./yang-modules/ietf-packet-fields@2019-03-04.yang +./yang-modules/ietf-te-packet-types@2025-01-24.yang +./yang-modules/tree.txt +./yang-modules/ietf-network-topology@2018-02-26.yang +./yang-modules/ietf-network-slice-service@2025-05-09.yang diff --git a/out-skipped.txt b/out-skipped.txt new file mode 100644 index 0000000..9a01149 --- /dev/null +++ b/out-skipped.txt @@ -0,0 +1,381 @@ +./.env +./out-skipped.txt +./README.md +./LICENSE +./out-updated.txt +./out-no-header.txt +./images/NSC_Architecture.png +./src/mapper/__pycache__/main.cpython-312.pyc +./src/mapper/__pycache__/slo_viability.cpython-312.pyc +./src/mapper/__pycache__/slo_viability.cpython-39.pyc +./src/mapper/__pycache__/main.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-312.pyc +./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-312.pyc +./src/realizer/ixia/__pycache__/main.cpython-312.pyc +./src/realizer/ixia/__pycache__/ixia_connect.cpython-312.pyc +./src/realizer/ixia/__pycache__/ixia_connect.cpython-39.pyc +./src/realizer/ixia/__pycache__/main.cpython-39.pyc +./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-312.pyc +./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-312.pyc +./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-39.pyc +./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-39.pyc +./src/realizer/e2e/__pycache__/e2e_connect.cpython-39.pyc +./src/realizer/e2e/__pycache__/main.cpython-312.pyc +./src/realizer/e2e/__pycache__/e2e_connect.cpython-312.pyc +./src/realizer/e2e/__pycache__/main.cpython-39.pyc +./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-312.pyc +./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-39.pyc +./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-312.pyc +./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-39.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-312.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-39.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-312.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-39.pyc +./src/realizer/tfs/__pycache__/main.cpython-312.pyc +./src/realizer/tfs/__pycache__/tfs_connect.cpython-39.pyc +./src/realizer/tfs/__pycache__/tfs_connect.cpython-312.pyc +./src/realizer/tfs/__pycache__/main.cpython-39.pyc +./src/realizer/__pycache__/send_controller.cpython-39.pyc +./src/realizer/__pycache__/main.cpython-312.pyc +./src/realizer/__pycache__/send_controller.cpython-312.pyc +./src/realizer/__pycache__/select_way.cpython-39.pyc +./src/realizer/__pycache__/select_way.cpython-312.pyc +./src/realizer/__pycache__/nrp_handler.cpython-312.pyc +./src/realizer/__pycache__/nrp_handler.cpython-39.pyc +./src/realizer/__pycache__/main.cpython-39.pyc +./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-312.pyc +./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-39.pyc +./src/planner/hrat_planner/__pycache__/hrat.cpython-312.pyc +./src/planner/hrat_planner/__pycache__/hrat.cpython-39.pyc +./src/planner/__pycache__/planner.cpython-39.pyc +./src/planner/__pycache__/planner.cpython-312.pyc +./src/planner/energy_planner/topo_ddbb.json +./src/planner/energy_planner/energy_ddbb.json +./src/planner/energy_planner/ext_topo_ddbb.json +./src/planner/energy_planner/__pycache__/energy.cpython-39.pyc +./src/planner/energy_planner/__pycache__/energy.cpython-312.pyc +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_1.json +./src/tests/requests/3gpp_template_UC1PoC2_midhaul.json +./src/tests/requests/l3vpn_test.json +./src/tests/requests/ietf_green_request.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul.json +./src/tests/requests/frr_request.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_2.json +./src/tests/requests/create_slice_1.json +./src/tests/requests/ietf_template_timing.json +./src/tests/requests/slice_request.json +./src/tests/requests/P2MP.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_3.json +./src/tests/requests/3ggpp_template_green.json +./src/config/IPs.json +./src/config/__pycache__/config.cpython-39.pyc +./src/config/__pycache__/constants.cpython-39.pyc +./src/config/__pycache__/config.cpython-312.pyc +./src/config/__pycache__/constants.cpython-312.pyc +./src/database/nrp_ddbb.json +./src/database/__pycache__/db.cpython-312.pyc +./src/database/__pycache__/sysrepo_store.cpython-39.pyc +./src/database/__pycache__/store_data.cpython-312.pyc +./src/database/__pycache__/db.cpython-39.pyc +./src/database/__pycache__/store_data.cpython-39.pyc +./src/database/__pycache__/sysrepo_store.cpython-312.pyc +./src/templates/ietfL3VPN_template_empty.json +./src/templates/L3-VPN_template_empty.json +./src/templates/Optical_slice.json +./src/templates/3gpp_template_empty.json +./src/templates/ietf_template_empty.json +./src/templates/TAPI_service.json +./src/templates/L2-VPN_template_empty.json +./src/templates/ietfL2VPN_template_empty.json +./src/templates/IPoWDM_orchestrator.json +./src/nbi_processor/__pycache__/main.cpython-312.pyc +./src/nbi_processor/__pycache__/translator.cpython-312.pyc +./src/nbi_processor/__pycache__/translator.cpython-39.pyc +./src/nbi_processor/__pycache__/detect_format.cpython-312.pyc +./src/nbi_processor/__pycache__/main.cpython-39.pyc +./src/nbi_processor/__pycache__/detect_format.cpython-39.pyc +./src/api/__pycache__/main.cpython-312.pyc +./src/api/__pycache__/main.cpython-39.pyc +./src/__pycache__/main.cpython-312.pyc +./src/__pycache__/main.cpython-39.pyc +./src/webui/static/img/MiniLogo.png +./src/webui/static/img/TelefonicaLogo.png +./src/webui/static/img/banner.png +./src/webui/__pycache__/gui.cpython-39.pyc +./src/webui/__pycache__/gui.cpython-312.pyc +./src/utils/__pycache__/dump_templates.cpython-39.pyc +./src/utils/__pycache__/safe_get.cpython-39.pyc +./src/utils/__pycache__/safe_get.cpython-312.pyc +./src/utils/__pycache__/build_response.cpython-312.pyc +./src/utils/__pycache__/load_template.cpython-312.pyc +./src/utils/__pycache__/send_response.cpython-312.pyc +./src/utils/__pycache__/dump_templates.cpython-312.pyc +./src/utils/__pycache__/build_response.cpython-39.pyc +./src/utils/__pycache__/load_template.cpython-39.pyc +./src/utils/__pycache__/send_response.cpython-39.pyc +./swagger/restconf_swagger_full.json +./swagger/restconf_swagger.json +./swagger/models/__pycache__/create_models.cpython-39.pyc +./swagger/models/__pycache__/create_models.cpython-312.pyc +./swagger/models/__pycache__/create_models_restconf.cpython-312.pyc +./swagger/__pycache__/E2E_namespace.cpython-39.pyc +./swagger/__pycache__/ixia_namespace.cpython-312.pyc +./swagger/__pycache__/tfs_namespace.cpython-312.pyc +./swagger/__pycache__/restconf_namespace.cpython-312.pyc +./swagger/__pycache__/tfs_namespace.cpython-39.pyc +./swagger/__pycache__/E2E_namespace.cpython-312.pyc +./swagger/__pycache__/ixia_namespace.cpython-39.pyc +./.gitlab/issue_templates/enhancement.md +./.gitlab/issue_templates/new-feature.md +./.gitlab/issue_templates/bug.md +./.gitlab/issue_templates/new-feature-with-design.md +./.git/index +./.git/HEAD +./.git/ORIG_HEAD +./.git/FETCH_HEAD +./.git/description +./.git/config +./.git/packed-refs +./.git/COMMIT_EDITMSG +./.git/objects/a1/c55879cb8c020c875f9237f726c4cf110297aa +./.git/objects/24/319c155fb104e20bee79e5b257317b01323197 +./.git/objects/d3/a1c91438954552d86907f8e2ab41a0cdf0c8e4 +./.git/objects/d9/da81eeebefad9054a43af552de3d51f20bcd56 +./.git/objects/bc/0bbade1bca1444abc8e4f834a7d0e3ad714bcc +./.git/objects/83/b8d3d4ad4e28b52e88d38a769f56109b5b8e54 +./.git/objects/13/364ab58163d44a3e6bbfbf25d7cb3b0f484e98 +./.git/objects/13/0ef01e5b6b9388458379a3602b1ec98374bc31 +./.git/objects/84/ffcc16d441fee227dd11117d87e2945cbb85d7 +./.git/objects/56/94d78e71d85b1da4eff3ad3a45af3acf808d9f +./.git/objects/56/930c81de3639098c8c42a511fafdb154c7b26e +./.git/objects/56/a52ec183ae5731cd400b47f3892bc3cdd93f6b +./.git/objects/ce/5604665f56b58b25593deeaf3c38db07db9ef3 +./.git/objects/c7/2736f92d46a5befe01e19a7a8d3049966952c8 +./.git/objects/68/4d191ecb38ed786b6be8faa19b58b1fc684c9b +./.git/objects/9a/50709c0681b663042d652f067c4929e7d61d4c +./.git/objects/9a/1e5b2c267e9226e3af34f8b2047bb9b38018ad +./.git/objects/b2/015d83dadd94e23940447d942d1d8ea6398ad3 +./.git/objects/2a/a4667a5e3db8e6de820394831a54d36fae96ed +./.git/objects/2a/62635019070c7a6edb0988ddc3216f844336ac +./.git/objects/2a/369c6bd4be9badb160d4bdee49cfb5ea605e0b +./.git/objects/30/623e9b710b9f32a386a0c1ff693d322a8cba88 +./.git/objects/ba/cdaf84f155b43df2b89ef0e4efc3d57b0d7fff +./.git/objects/0c/6e465d74277bed20db3cbbb8d6bc9e9c041e65 +./.git/objects/29/5a0d32051bb651cc0dcdd6fefae0468094a7c6 +./.git/objects/1e/c944d791db1da1b8236c6069f10d65b1b6f97f +./.git/objects/1e/6bf8411348d038dc8ab93114c0ec787f2fac1f +./.git/objects/1e/8cf0817554ec18499a53d9c401a3f54d6d49e5 +./.git/objects/c9/ae55eb708854fd67b73410a4f8715e64b3aa57 +./.git/objects/6c/ea7d3642dab6ba8ac09f3e44a07d6591ba99f4 +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.idx +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.rev +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.idx +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.pack +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.idx +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.pack +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.rev +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.pack +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.idx +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.rev +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.pack +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.rev +./.git/objects/a9/1024508763398543610cea7b8e16fa952d28b7 +./.git/objects/a9/cd1d4586eb8befff4a7e90df781eb7806aed39 +./.git/objects/f8/0e69f3bdf2ec564923cfaba9b88ac70882b77c +./.git/objects/f8/00d42dbcc2f59c613d1007c4738e2acb0355d2 +./.git/objects/8f/37e643276bf994d955e3dad95a4cb93c258f67 +./.git/objects/8f/132c895e2946db20e93bd54149330ac0226bfb +./.git/objects/17/5b00798abd846f9413b56e1d3de4937f832684 +./.git/objects/17/eb5d4435e7efcc94acf8e32506c0705cedb8e5 +./.git/objects/80/0c62fe8e5147086dd9f07d03aabaf297a3287f +./.git/objects/e3/8daf12a35eda16070034191c7a2c5220c5e057 +./.git/objects/e3/019e59e794d66493817c9d76c75ba7718ec4df +./.git/objects/da/3ef7a52980a769f2ed10e1e1167995af16576c +./.git/objects/da/857ab54a12b2dbe6315042b7bdfccc1aff19cc +./.git/objects/50/b00fbe85eeaa64bcbe8159c3d04164d3ea9113 +./.git/objects/d1/f11cd63da20457265e6a159531c05b8b0ea75e +./.git/objects/82/b67a1ca7cf7b568cb9fa566a6ab065072aaa2e +./.git/objects/5e/79c73f4c3a8a1be7f0cb84f37d0cf93a9b6da0 +./.git/objects/5e/da6a83ccffc29200838e23f5b7feecbc3aa8ce +./.git/objects/5e/fe8d47ca5f0ec8c2f89d4dbbf91de604da0565 +./.git/objects/7f/24267ce03103755f28099cc124c0a821fd739e +./.git/objects/7f/6f0a8a616fc2eb18dbd69255ff35ef43041931 +./.git/objects/cf/da9cf8b25de8bc8dfbe63f728e5e929e6bd66b +./.git/objects/cf/f183b0d73d0a799af3a09f19583ecc6e780833 +./.git/objects/cf/aa9ee46f37057b0cba026a5d042d1ef3f6438d +./.git/objects/7c/c66d11d7ba06287896a33b2c018c2061e22851 +./.git/objects/87/28250df41f46adc8fa6b412e676c3e0117fa41 +./.git/objects/43/0c261a579e911ee8742490dbc87238a1c1cc08 +./.git/objects/4d/ebe53ee691012be9a33b44b6ee508315db8ca9 +./.git/objects/eb/56ca633390c63c058c351e3c9492b6ef5374f5 +./.git/objects/d2/46c96bd2ff97920f294fb6b0213cc7c70137c2 +./.git/objects/7b/ce196c1667ee6c8f708e674a7f8e20724ecfc0 +./.git/objects/65/d4040710f3628da6fe84d5a538c1675fb1ccb9 +./.git/objects/af/3bafe1661a00bb7cfc9f44157f34af84befa28 +./.git/objects/00/396bbf60369411bfb43b7a84a87f19facc5dee +./.git/objects/45/4eb176fe183a1a1eed5826548cc95f16fc18aa +./.git/objects/45/bb5358f6219afdd12de9058c0917fb8823b4a4 +./.git/objects/9c/e5fa89b675aa014166c81b58e73848aaf6042a +./.git/objects/ca/57941fc5a97914a1727ab42a988a8e8e3467ff +./.git/objects/52/a393230b2fb369d6954208a7504efb830442a9 +./.git/objects/52/d85bce8df704dc8890ddc79ecb6dcfd1295159 +./.git/objects/4b/4ae60160daa6c1e12ab2ad325e130ac62f557f +./.git/objects/8c/e41b9930a05732402ee3d8661f497297b6750f +./.git/objects/8c/ef7eeb08caf1349d4f1cdd3b52b8d01132e151 +./.git/objects/5a/aa083f84abd1c8ca2ffcb7d5c19bd63600611c +./.git/objects/a6/461e6335cfe0b97521db324595cb489fc1c41b +./.git/objects/2f/efbaab653094924acb3b0156c1eab15e752f85 +./.git/objects/2f/b797bd87bf4ed825f83ec788df707b94c5f68b +./.git/objects/e1/f13a86f6084d0906a384b0421385bb829d89d0 +./.git/objects/86/3e3ab7d7f0da019483b0293706569fdb295bc6 +./.git/objects/75/704a402d5653bdeb8b0bc90c011d67f1d0c3cf +./.git/objects/b7/dc48c63a8954330f2f8246e423dfb56a8563f5 +./.git/objects/41/baeecff513727319d7b0f395e822ecfd1fedc4 +./.git/objects/41/a7996ce90a56369c30484b143c931fd18b4518 +./.git/objects/c0/eca0991b29295a0afeaaa239d3b25aa2551d3d +./.git/objects/1a/2966234be8d0a24ffeead6dfb2525f645dcdbf +./.git/objects/1a/650c3076c03324bfdfe6754a125f0e1fa8a145 +./.git/objects/95/2b856ba4244babb1dab96d3698d0f286e073cd +./.git/objects/35/f862c574b53d55da5c4db62e29b305716b5d90 +./.git/objects/fd/055074aeba5c277bbefdce0b81ebd24d0d3551 +./.git/objects/96/5ddb39541fe96b8c71683b4ad5b63a08316daa +./.git/objects/96/418bb720ff04b7a36ee47b45a570692a7278a8 +./.git/objects/c2/9f6a40d317311ce82e756304033d5dd105c390 +./.git/objects/fc/9940922faff4ccccd2eaf705ed94f9982ac047 +./.git/objects/fc/16b40610986249153e18fc9650469671b46b95 +./.git/objects/90/ed7fa37077ddad67e2f82d7881df2f617be577 +./.git/objects/6f/3933c422c188054aeec25d640541348bde5fdd +./.git/objects/6f/7ec43b9fc24a5f9cc2de84422ad4f027fec93d +./.git/objects/60/7f11e8e85f7b241937efdf91a4f43b14e65ab9 +./.git/objects/25/67a613ebdf545a1f1bd650566ba947162efc35 +./.git/objects/36/83a2581ba7d2517a0b5e59edc594893ae6d99c +./.git/objects/36/f8d72d0627516b64420ed0bc1175700f711857 +./.git/objects/51/abd33613baeebbad2202efca9e022c6d55149f +./.git/objects/51/47a8e3ae3343937f77bc56f3b3048988e35404 +./.git/objects/0a/28bb8a469ecce2bacd0e3d57255964cad4e7ca +./.git/objects/2e/ba42828c5ff7ae54dd242beab47700b79b987d +./.git/objects/f5/fcc3572df93b2752b1ed393cb09e0c501f7a15 +./.git/objects/ef/16b4394fee65ab326bee56e80c5069a844992c +./.git/objects/67/be760a1c939c84bb1310a22ff1f35ea9c9abb0 +./.git/objects/8b/e0afbc8d5f2cadc93eb685a4f210aab0458de2 +./.git/objects/33/45a89c51c736f929344b68f897e560fb7798e0 +./.git/objects/33/5b7e601c09e5751a80f346c9aafb93d17101d2 +./.git/objects/72/05a1fdf91869d35795d35df9a44fcf0a365e3f +./.git/objects/c5/59d204968a27b32f5eb3235a91b07317749239 +./.git/objects/93/2d002794afabda8f14c595d1e9644943ca2757 +./.git/objects/44/5d1994a5ac57366078b198200a9e143d4ccda8 +./.git/objects/5f/d71bb7aba1ce2209373b626e99e8b7d6f4c03f +./.git/objects/0d/65d4ea9dbfe0e79f437e9efbd78c56ada25688 +./.git/objects/b9/3f0df8f29b3d8070ed2a9f54bbe31029fe66e1 +./.git/objects/b9/7cd799d77968fdc4cdfef22365468470cf0eb4 +./.git/objects/4f/fb116001eeded5674daef6abef65089208ed1a +./.git/objects/ff/b1e0654e644d404272145c9e82d40c70da3fcf +./.git/objects/fb/df8b0da5f9b93d54bb5f221c3bc97704675004 +./.git/objects/6b/680462719b3b632f1a37f89ce79005df57ef23 +./.git/objects/6b/ffc7308bc85dde17a094c88e4db4c055f72488 +./.git/objects/6b/1c053d37cecbcb581c74be40a7b0a76d450575 +./.git/objects/85/d23814e172addd8c22c0f8fa0bd2f2a62212f5 +./.git/objects/b6/098d3a5d03e998d50c9b87b06c3c1940ee7c4f +./.git/objects/bf/4855faf0508a152471f6c6c8f756581b8ebb96 +./.git/objects/bf/45cf47afbe1af29b37202ffa7aad670f724315 +./.git/objects/c8/2b2f7a810646753627819a01e54b2fbb0bf994 +./.git/objects/fa/64f14a857fee267529d204b1459599cdcd4c1c +./.git/objects/64/76ab8696fde6f30a60496b6432e722be4f7ecb +./.git/objects/64/bc3be12e5a19ba7b8d2feef676f87de847c7ee +./.git/objects/31/9ae2c46027c14596f9cbb2b7feac7638a53ac3 +./.git/objects/63/5ac650b9e5cb5102933f3b31acd333c5a80893 +./.git/objects/63/e6828bfe6daec53b28903951dba03be01d3030 +./.git/objects/79/eb1ce74e5351a6f490e0982cbc6e129b4aa7c8 +./.git/objects/5c/ca0f79b82611f512e6294a0f00596a234eb85c +./.git/objects/b3/16e45895dfd681cc04ee3301c329a74fdba090 +./.git/objects/1f/8147d384285cfe5ff18e96ebeff64aca142ab2 +./.git/objects/3b/4c4f812a8d80d92927085740fee2bafe5a602f +./.git/objects/23/16ac2bb4ceac9df62c1ff1e1a8a7bdcb01e5d7 +./.git/objects/9b/3926088eae0e151326f6f0015829521df010b4 +./.git/objects/27/8591987f33b59eb256926e6d9c96a0cdb40b92 +./.git/objects/d5/a0cd040e7aba6e44e5cb6c48b3b31de7fa3996 +./.git/objects/d5/61671aaa8c1913d335a6750e9fad59f6065448 +./.git/objects/d5/d06687674c7ac43f59839c7d16ffe001f7d0c7 +./.git/objects/d5/8b47cc8995eda0d0902699f1037849b620e723 +./.git/objects/d5/4fbe58d8f2468a576a9cec2bcaaa26801f6b3d +./.git/objects/d5/45ab9b4098ec8c1818861356b9fe334552af1a +./.git/objects/fe/ea2538a9529bd851c99b976099e226409080c3 +./.git/objects/fe/4849b2f3a88b6f3997f565ad7d9da97c402a58 +./.git/objects/fe/b44f5214740a873ac5eedbb942834d8916e07d +./.git/objects/55/3aa665e46a83e53e331b2435e4eacd6e696ba0 +./.git/objects/55/b6eb9fc430d168b02cbf5a231d93bb7632ab31 +./.git/objects/a0/e4208fa05edf37e2f64955495172d860048b74 +./.git/objects/7e/db6adeb6c45b5b3524c1c33d2c1abdfd2ef4b3 +./.git/objects/c4/507da960010d98b9560a34fe55686858f1116b +./.git/objects/38/a157dfa5222bb8aad0c3531a07c0eb288a9f0a +./.git/objects/b8/15446f819ac2dcba50e63e5781e8a1be9b59c6 +./.git/objects/74/d094765ec06d081a8eb0cf62a2e6f3812829a0 +./.git/objects/d4/7bcee05a434c6b87726710fcc854971a72d606 +./.git/objects/09/cad2ec21e916c54b3aebb07431df19760c0e25 +./.git/objects/a4/a10b59d02c8c64af41d0bb0255873e888ea89d +./.git/objects/e0/1a219d20f0a4e2a6fb4747565f072e89dbe3ee +./.git/objects/53/27cdf631d950ea1e10d0517bdcd67179e9721f +./.git/objects/37/30fb786f1b523cd179652a96f821ce88eea36d +./.git/objects/37/650c30d77f69b0b011040293e053a4e7c6f98c +./.git/objects/28/f953b838d0200c67301cc7f3a0d352e26f3c42 +./.git/objects/f2/d19117ef0cd800c52fde833c32acb71e46a664 +./.git/objects/4c/5dd0a4298e91212e5422fa71254fc0e5176158 +./.git/objects/4c/be96ff54343b2f28a2b5069343ec49a4342ba9 +./.git/objects/a8/d605fb18e5c5ee1f3f5352c1460e1f760535bd +./.git/objects/a8/d7d38e73f05bf78472f59a092bdef3949483e7 +./.git/objects/b0/165037a79df96cdc3570f14250ae79bb3e693e +./.git/objects/b0/af64a3d1d8dc88b08bb9208dea14f8761f973f +./.git/objects/2b/38518e1f2a3349a23737edb17748c0ec1428cc +./.git/objects/2b/52a895406f2999f2fed4bbb21dcd33d7569d6a +./.git/objects/b4/80245fb3f89a446b36d21c8b1b6df792e2e5a8 +./.git/objects/7a/6d78346ba3078860f945a4f56f88a8e50d2b42 +./.git/objects/58/f8841a3fc96b9e14be040842de44b85e5276cc +./.git/objects/b1/8e4237f7f9851fbf4180cd4d0549497c165637 +./.git/objects/b1/afe94b6b0a3bd565aabc05acd34e487b914071 +./.git/objects/c1/df659fb1b5539c087fa05da398c1ffa81712b8 +./.git/objects/c1/eb94b7d6938b2ec524f98d5b6ea9276139d26d +./.git/objects/c1/b79b5e4a0d723e1c8a437c21bba6846b39a9c8 +./.git/objects/81/3380a694d4f4961a9feea10d881c3554d41155 +./.git/objects/81/cedd6d7915b885fefd382950534d4d5ee4d2de +./.git/objects/e2/d340bf4d50c22789fcae8145de3d53e9487bc8 +./.git/objects/e2/2ce8b7aa5354f50987e9646e7b20274d7d8f3f +./.git/objects/e2/6091b8dfab7cd3efd7f0ce8f7b1dc8bd540f6f +./.git/objects/9e/73b99fe785e2c18588575ebea9e63409e6390b +./.git/objects/34/3455561e55393181b9ad342eedb62b242a59ca +./.git/objects/2d/9823652e7ca859d5f826b475c4d7a9bc979919 +./.git/logs/HEAD +./.git/logs/refs/heads/main +./.git/logs/refs/heads/develop +./.git/logs/refs/heads/feat/25-tid-improve-mapping-of-slice-requests +./.git/logs/refs/heads/feat/24-tid-restconf-base-nbi +./.git/logs/refs/remotes/origin/sat_module +./.git/logs/refs/remotes/origin/HEAD +./.git/logs/refs/remotes/origin/develop +./.git/logs/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests +./.git/logs/refs/remotes/origin/feat/24-tid-restconf-base-nbi +./.git/refs/heads/main +./.git/refs/heads/develop +./.git/refs/heads/feat/25-tid-improve-mapping-of-slice-requests +./.git/refs/heads/feat/24-tid-restconf-base-nbi +./.git/refs/remotes/origin/sat_module +./.git/refs/remotes/origin/HEAD +./.git/refs/remotes/origin/develop +./.git/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests +./.git/refs/remotes/origin/feat/24-tid-restconf-base-nbi +./.git/info/exclude +./.git/hooks/post-update.sample +./.git/hooks/pre-push.sample +./.git/hooks/sendemail-validate.sample +./.git/hooks/pre-receive.sample +./.git/hooks/pre-merge-commit.sample +./.git/hooks/pre-rebase.sample +./.git/hooks/update.sample +./.git/hooks/pre-applypatch.sample +./.git/hooks/fsmonitor-watchman.sample +./.git/hooks/push-to-checkout.sample +./.git/hooks/applypatch-msg.sample +./.git/hooks/pre-commit.sample +./.git/hooks/commit-msg.sample +./.git/hooks/prepare-commit-msg.sample diff --git a/out-updated.txt b/out-updated.txt new file mode 100644 index 0000000..6953feb --- /dev/null +++ b/out-updated.txt @@ -0,0 +1,88 @@ +./Dockerfile +./.gitignore +./app.py +./deploy.sh +./.dockerignore +./.gitlab-ci.yml +./requirements.txt +./src/main.py +./src/mapper/process_connnectivity.py +./src/mapper/main.py +./src/mapper/get_service_template.py +./src/mapper/get_template.py +./src/mapper/slo_viability.py +./src/mapper/extract_sdp_info.py +./src/realizer/nrp_handler.py +./src/realizer/main.py +./src/realizer/send_controller.py +./src/realizer/select_way.py +./src/realizer/ixia/ixia_connect.py +./src/realizer/ixia/main.py +./src/realizer/ixia/helpers/automatizacion_ne2v4.py +./src/realizer/ixia/helpers/NEII_V4.py +./src/realizer/restconf/restconf_connect.py +./src/realizer/restconf/main.py +./src/realizer/restconf/connectors/tfs_connector.py +./src/realizer/restconf/connectors/frr_connector.py +./src/realizer/restconf/connectors/cisco_connector.py +./src/realizer/restconf/service_types/l3vpn.py +./src/realizer/restconf/service_types/l2vpn.py +./src/realizer/restconf/service_types/builders/configure_match_criteria.py +./src/realizer/restconf/service_types/builders/create_network_access.py +./src/realizer/restconf/service_types/builders/configure_slos.py +./src/realizer/restconf/service_types/builders/create_site_from_sdp.py +./src/realizer/restconf/service_types/builders/initialize_structure.py +./src/realizer/restconf/service_types/builders/apply_metric_constraint.py +./src/realizer/e2e/e2e_connect.py +./src/realizer/e2e/main.py +./src/realizer/e2e/service_types/del_l3ipowdm_slice.py +./src/realizer/e2e/service_types/l3ipowdm_slice.py +./src/realizer/tfs/tfs_connect.py +./src/realizer/tfs/main.py +./src/realizer/tfs/helpers/tfs_connector.py +./src/realizer/tfs/helpers/cisco_connector.py +./src/realizer/tfs/service_types/tfs_l2vpn.py +./src/realizer/tfs/service_types/tfs_l3vpn.py +./src/planner/planner.py +./src/planner/tfs_optical_planner/tfs_optical.py +./src/planner/hrat_planner/hrat.py +./src/planner/energy_planner/energy.py +./src/tests/test_e2e.py +./src/tests/test_mapper.py +./src/tests/test_api.py +./src/tests/test_utils.py +./src/tests/test_nbi_processor.py +./src/tests/test_database.py +./src/tests/test_initialization.py +./src/config/config.py +./src/config/.env.example +./src/config/constants.py +./src/database/store_data.py +./src/database/db.py +./src/database/sysrepo_store.py +./src/nbi_processor/detect_format.py +./src/nbi_processor/main.py +./src/nbi_processor/translator.py +./src/api/main.py +./src/webui/gui.py +./src/webui/templates/dev.html +./src/webui/templates/search.html +./src/webui/templates/login.html +./src/webui/templates/index.html +./src/webui/templates/welcome.html +./src/webui/templates/ixia.html +./src/utils/build_response.py +./src/utils/send_response.py +./src/utils/load_template.py +./src/utils/safe_get.py +./src/utils/dump_templates.py +./src/utils/slice_manager.py +./swagger/tfs_namespace.py +./swagger/ixia_namespace.py +./swagger/restconf_namespace.py +./swagger/E2E_namespace.py +./swagger/models/create_models_restconf.py +./swagger/models/create_models.py +./scripts/update_license_headers.py +./scripts/dump_templates_nsc.sh +./scripts/show_logs_nsc.sh diff --git a/requirements.txt b/requirements.txt index 555cf5a..d8af18e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/scripts/dump_templates_nsc.sh b/scripts/dump_templates_nsc.sh index b93f0df..33b5f1d 100644 --- a/scripts/dump_templates_nsc.sh +++ b/scripts/dump_templates_nsc.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/scripts/show_logs_nsc.sh b/scripts/show_logs_nsc.sh index 53a02fc..7c2f068 100755 --- a/scripts/show_logs_nsc.sh +++ b/scripts/show_logs_nsc.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/scripts/update_license_headers.py b/scripts/update_license_headers.py new file mode 100644 index 0000000..714a70c --- /dev/null +++ b/scripts/update_license_headers.py @@ -0,0 +1,165 @@ +#!/bin/bash +# Copyright 2022-2026 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. + +# Run from ~/tfs-ctrl as: +# python scripts/update_license_headers.py + +import logging, os, re, sys +from io import TextIOWrapper + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + +ROOT_PATH = '.' +FILE_PATH_SKIPPED = 'out-skipped.txt' +FILE_PATH_NO_HEADER = 'out-no-header.txt' +FILE_PATH_UPDATED = 'out-updated.txt' + +STR_NEW_COPYRIGHT = 'Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)' + +RE_OLD_COPYRIGHTS = [ + r'Copyright\ 2021\-202[0-9]\ H2020\ TeraFlow\ \(https\:\/\/www\.teraflow\-h2020\.eu\/\)', + r'Copyright\ 2022\-202[0-9]\ ETSI\ TeraFlowSDN\ \-\ TFS\ OSG\ \(https\:\/\/tfs\.etsi\.org\/\)', + r'Copyright\ 2022\-202[0-9]\ ETSI\ TeraFlowSDN\ \-\ TFS\ OSG\/SDG\ \(https\:\/\/tfs\.etsi\.org\/\)', + r'Copyright\ 2022\-202[0-9]\ ETSI\ OSG\/SDG\ TeraFlowSDN\ \(TFS\)\ \(https\:\/\/tfs\.etsi\.org\/\)', + r'Copyright\ 2022\-202[0-5]\ ETSI\ SDG\ TeraFlowSDN\ \(TFS\)\ \(https\:\/\/tfs\.etsi\.org\/\)', +] +RE_OLD_COPYRIGHTS = [ + (re.compile(r'.*{}.*'.format(re_old_copyright)), re.compile(re_old_copyright)) + for re_old_copyright in RE_OLD_COPYRIGHTS +] + +def skip_file(file_path : str) -> bool: + if os.path.islink(file_path): return True + if file_path.endswith('.pyc'): return True + if file_path.endswith('_pb2_grpc.py'): return True + if file_path.endswith('_pb2.py'): return True + if file_path.endswith('.class'): return True + if file_path.endswith('.md'): return True + if file_path.endswith('.png'): return True + if file_path.endswith('.json'): return True + if file_path.endswith('.csv'): return True + if file_path.endswith('.zip'): return True + if file_path.endswith('.zip'): return True + if file_path.endswith('.tgz'): return True + if file_path.endswith('.jar'): return True + if file_path.endswith('.onnx'): return True + if file_path.endswith('/tstat'): return True + if file_path.endswith('/coverage/.coverage'): return True + if file_path.endswith('/grpc/grpcurl/grpcurl'): return True + if file_path.endswith('/probe/probe-tfs/target/x86_64-unknown-linux-musl/release/tfsagent'): return True + if file_path.endswith('/probe/probe-tfs/target/x86_64-unknown-linux-musl/release/tfsping'): return True + if file_path.startswith('./libyang/'): return True + if file_path.startswith('./netconf_openconfig/'): return True + if file_path.startswith('./tmp/'): return True + if '/manifests/cttc-ols/' in file_path: return True + if '/hackfest/netconf-oc/openconfig/' in file_path: return True + if '/hackfest/tapi/server/tapi_server/' in file_path: return True + if '/hackfest/kafka/kafka_2.13-2.8.0/' in file_path: return True + if '/hackfest5/clab-hackfest5/' in file_path: return True + if '/.git/' in file_path: return True + if '/.vscode/' in file_path: return True + if '/.pytest_cache/' in file_path: return True + if '/__pycache__/' in file_path: return True + if '/.mvn/' in file_path: return True + if '/bgpls_speaker/service/java/netphony-topology/.settings/' in file_path: return True + if '/bgpls_speaker/service/java/netphony-topology/' in file_path: + file_path_parts = file_path.split('/') + file_name = file_path_parts[-1] + return file_name in {'LICENSE', 'VERSION'} + if '/device/service/drivers/gnmi_openconfig/git/' in file_path: return True + if '/device/service/drivers/gnmi_openconfig/gnmi/gnmi' in file_path: return True + if '/device/service/drivers/gnmi_openconfig/gnmi/Acknowledgement.txt' in file_path: return True + if '/device/service/drivers/openconfig/templates/' in file_path: + if file_path.endswith('.xml'): return True + file_path_parts = file_path.split('/') + file_name = file_path_parts[-1] + if file_name.startswith('openconfig_') and file_name.endswith('.py'): return True + return False + if '/device/service/drivers/smartnic/' in file_path: + if file_path.startswith('openconfig-'): return True + if file_path.startswith('ietf-'): return True + if file_path.endswith('references_probes_libraries.txt'): return True + return False + if '/nbi/service/ietf_network/bindings/' in file_path: return True + if '/nbi/service/ietf_network_slice/bindings/' in file_path: return True + if '/nbi/service/ietf_l3vpn/yang/' in file_path: return True + if '/nbi/service/ietf_network/yang/' in file_path: return True + if '/tests/tools/mock_qkd_nodes/yang/' in file_path: return True + if '/ztp/target/' in file_path: return True + if '/policy/target/' in file_path: return True + if '/dlt/gateway/_legacy' in file_path: return True + if '/proto/src/python/asyncio/' in file_path: return True + if FILE_PATH_SKIPPED in file_path: return True + if FILE_PATH_NO_HEADER in file_path: return True + if FILE_PATH_UPDATED in file_path: return True + if file_path in {'./LICENSE', './.python-version', './.env'}: return True + return False + +def process_line(line_in : str) -> str: + for re_match, re_sub in RE_OLD_COPYRIGHTS: + match = re_match.match(line_in) + if match is None: continue + line_out = re_sub.sub(STR_NEW_COPYRIGHT, line_in) + return line_out + return line_in + +def process_file( + file_path : str, file_no_header : TextIOWrapper, file_skipped : TextIOWrapper, file_updated : TextIOWrapper +) -> None: + if skip_file(file_path): + file_skipped.write(file_path + '\n') + return + + LOGGER.info(' File {:s}...'.format(str(file_path))) + + temp_file_path = file_path + '.temp' + replaced = False + file_stat = os.stat(file_path) + try: + with open(file_path, encoding='UTF-8', newline='') as source: + with open(temp_file_path, 'w', encoding='UTF-8', newline='') as target: + for line_in in source: + if STR_NEW_COPYRIGHT in line_in: + replaced = True + target.write(line_in) + else: + line_out = process_line(line_in) + replaced = replaced or (line_out != line_in) + target.write(line_out) + except: # pylint: disable=bare-except + replaced = False + + if not replaced: + file_no_header.write(file_path + '\n') + else: + file_updated.write(file_path + '\n') + + os.rename(temp_file_path, file_path) + os.chmod(file_path, file_stat.st_mode) + +def main() -> int: + with open(FILE_PATH_NO_HEADER, 'w', encoding='UTF-8') as file_no_header: + with open(FILE_PATH_SKIPPED, 'w', encoding='UTF-8') as file_skipped: + with open(FILE_PATH_UPDATED, 'w', encoding='UTF-8') as file_updated: + for dirpath, _, filenames in os.walk(ROOT_PATH): + LOGGER.info('Folder {:s}...'.format(str(dirpath))) + for filename in filenames: + file_path = os.path.join(dirpath, filename) + process_file(file_path, file_no_header, file_skipped, file_updated) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/api/main.py b/src/api/main.py index 236f019..1d4dacb 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/config/.env.example b/src/config/.env.example index ce04227..513c6db 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -17,9 +17,9 @@ # ------------------------- # General # ------------------------- -NSC_PORT=8086 +NSC_PORT=8085 # Options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET -LOGGING_LEVEL=INFO +LOGGING_LEVEL=DEBUG DUMP_TEMPLATES=false # ------------------------- @@ -42,14 +42,14 @@ OPTICAL_PLANNER_IP=10.0.0.1 # Realizer # ------------------------- # If true, no config sent to controllers -DUMMY_MODE=true +DUMMY_MODE=false # ------------------------- # Teraflow # ------------------------- TFS_IP=127.0.0.1 # Options: WEBUI or NBI -UPLOAD_TYPE=WEBUI +UPLOAD_TYPE=NBI # Flag to determine if additional L2VPN configuration support is required for deploying L2VPNs with path selection TFS_L2VPN_SUPPORT=false @@ -66,11 +66,11 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=127.0.0.1 +RESTCONF_IP=192.168.27.165 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO -DATAPLANE_SUPPORT=FRR +DATAPLANE_SUPPORT=CISCO # ------------------------- # WebUI diff --git a/src/config/config.py b/src/config/config.py index ec8a5aa..f5cb327 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/config/constants.py b/src/config/constants.py index 195ce85..279d459 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -18,7 +18,7 @@ from pathlib import Path import os # Default port for NSC deployment -NSC_PORT = os.getenv("NSC_PORT", "8086") +NSC_PORT = os.getenv("NSC_PORT", "8085") # Paths BASE_DIR = Path(__file__).resolve().parent.parent.parent diff --git a/src/database/db.py b/src/database/db.py index 341d79e..f5be792 100644 --- a/src/database/db.py +++ b/src/database/db.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/database/store_data.py b/src/database/store_data.py index 17eb5d4..97a2c20 100644 --- a/src/database/store_data.py +++ b/src/database/store_data.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/database/sysrepo_store.py b/src/database/sysrepo_store.py index 63c80ba..22b08a6 100644 --- a/src/database/sysrepo_store.py +++ b/src/database/sysrepo_store.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/main.py b/src/main.py index 55b3fca..ba64187 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/extract_sdp_info.py b/src/mapper/extract_sdp_info.py index 7d5172e..dafbcf5 100644 --- a/src/mapper/extract_sdp_info.py +++ b/src/mapper/extract_sdp_info.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/get_service_template.py b/src/mapper/get_service_template.py index f67d1b7..d7b5976 100644 --- a/src/mapper/get_service_template.py +++ b/src/mapper/get_service_template.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/get_template.py b/src/mapper/get_template.py index db5ebe7..a4e5e5a 100644 --- a/src/mapper/get_template.py +++ b/src/mapper/get_template.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/main.py b/src/mapper/main.py index 1732419..6c6601d 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/process_connnectivity.py b/src/mapper/process_connnectivity.py index a85bd95..8653124 100644 --- a/src/mapper/process_connnectivity.py +++ b/src/mapper/process_connnectivity.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/mapper/slo_viability.py b/src/mapper/slo_viability.py index a91b929..3634a17 100644 --- a/src/mapper/slo_viability.py +++ b/src/mapper/slo_viability.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/nbi_processor/detect_format.py b/src/nbi_processor/detect_format.py index 2b48be3..1bb834f 100644 --- a/src/nbi_processor/detect_format.py +++ b/src/nbi_processor/detect_format.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/nbi_processor/main.py b/src/nbi_processor/main.py index cfa55dd..2e1787c 100644 --- a/src/nbi_processor/main.py +++ b/src/nbi_processor/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/nbi_processor/translator.py b/src/nbi_processor/translator.py index 335b7e6..79c1aaf 100644 --- a/src/nbi_processor/translator.py +++ b/src/nbi_processor/translator.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/planner/energy_planner/energy.py b/src/planner/energy_planner/energy.py index c6d33c8..5f23c5c 100644 --- a/src/planner/energy_planner/energy.py +++ b/src/planner/energy_planner/energy.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/planner/hrat_planner/hrat.py b/src/planner/hrat_planner/hrat.py index 0b370d7..9fd147b 100644 --- a/src/planner/hrat_planner/hrat.py +++ b/src/planner/hrat_planner/hrat.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/planner/planner.py b/src/planner/planner.py index 9797834..c856d0b 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/planner/tfs_optical_planner/tfs_optical.py b/src/planner/tfs_optical_planner/tfs_optical.py index 0d5bc79..25e344a 100644 --- a/src/planner/tfs_optical_planner/tfs_optical.py +++ b/src/planner/tfs_optical_planner/tfs_optical.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/e2e/e2e_connect.py b/src/realizer/e2e/e2e_connect.py index 368ac3d..b078523 100644 --- a/src/realizer/e2e/e2e_connect.py +++ b/src/realizer/e2e/e2e_connect.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/e2e/main.py b/src/realizer/e2e/main.py index 6eed883..ed63429 100644 --- a/src/realizer/e2e/main.py +++ b/src/realizer/e2e/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/e2e/service_types/del_l3ipowdm_slice.py b/src/realizer/e2e/service_types/del_l3ipowdm_slice.py index 8728250..4eb0557 100644 --- a/src/realizer/e2e/service_types/del_l3ipowdm_slice.py +++ b/src/realizer/e2e/service_types/del_l3ipowdm_slice.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/e2e/service_types/l3ipowdm_slice.py b/src/realizer/e2e/service_types/l3ipowdm_slice.py index 4fadf44..6e1d9c1 100644 --- a/src/realizer/e2e/service_types/l3ipowdm_slice.py +++ b/src/realizer/e2e/service_types/l3ipowdm_slice.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/ixia/helpers/NEII_V4.py b/src/realizer/ixia/helpers/NEII_V4.py index e9bf61a..4f9a249 100644 --- a/src/realizer/ixia/helpers/NEII_V4.py +++ b/src/realizer/ixia/helpers/NEII_V4.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/ixia/helpers/automatizacion_ne2v4.py b/src/realizer/ixia/helpers/automatizacion_ne2v4.py index fd8fc4f..65e6a33 100644 --- a/src/realizer/ixia/helpers/automatizacion_ne2v4.py +++ b/src/realizer/ixia/helpers/automatizacion_ne2v4.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/ixia/ixia_connect.py b/src/realizer/ixia/ixia_connect.py index 456d23a..107d985 100644 --- a/src/realizer/ixia/ixia_connect.py +++ b/src/realizer/ixia/ixia_connect.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/ixia/main.py b/src/realizer/ixia/main.py index 6f7ec43..5bcfcce 100644 --- a/src/realizer/ixia/main.py +++ b/src/realizer/ixia/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/main.py b/src/realizer/main.py index 2e22403..393d025 100644 --- a/src/realizer/main.py +++ b/src/realizer/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/nrp_handler.py b/src/realizer/nrp_handler.py index fa08d8d..f17f2c9 100644 --- a/src/realizer/nrp_handler.py +++ b/src/realizer/nrp_handler.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/connectors/cisco_connector.py b/src/realizer/restconf/connectors/cisco_connector.py index 4812006..9136b16 100644 --- a/src/realizer/restconf/connectors/cisco_connector.py +++ b/src/realizer/restconf/connectors/cisco_connector.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/connectors/frr_connector.py b/src/realizer/restconf/connectors/frr_connector.py index 4df772a..cebb3ee 100644 --- a/src/realizer/restconf/connectors/frr_connector.py +++ b/src/realizer/restconf/connectors/frr_connector.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/connectors/tfs_connector.py b/src/realizer/restconf/connectors/tfs_connector.py index 91c64f4..a4f2139 100644 --- a/src/realizer/restconf/connectors/tfs_connector.py +++ b/src/realizer/restconf/connectors/tfs_connector.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/main.py b/src/realizer/restconf/main.py index cf415d3..ac9dad9 100644 --- a/src/realizer/restconf/main.py +++ b/src/realizer/restconf/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/restconf_connect.py b/src/realizer/restconf/restconf_connect.py index aad4a05..9678e4f 100644 --- a/src/realizer/restconf/restconf_connect.py +++ b/src/realizer/restconf/restconf_connect.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/service_types/builders/apply_metric_constraint.py b/src/realizer/restconf/service_types/builders/apply_metric_constraint.py index 473f7b2..06f799e 100644 --- a/src/realizer/restconf/service_types/builders/apply_metric_constraint.py +++ b/src/realizer/restconf/service_types/builders/apply_metric_constraint.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index 4a9567c..ffff478 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/service_types/builders/configure_slos.py b/src/realizer/restconf/service_types/builders/configure_slos.py index 1225484..6a4f5d1 100644 --- a/src/realizer/restconf/service_types/builders/configure_slos.py +++ b/src/realizer/restconf/service_types/builders/configure_slos.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -39,4 +39,10 @@ def configure_slos(network_access, ietf_intent, layer_type): # Configurar MTU mtu = safe_get(ietf_intent, ["template", "slo-policy", "mtu"]) if mtu: - service["svc-mtu"] = mtu \ No newline at end of file + service["svc-mtu"] = mtu + + # Configurar availability y MTU por defecto si no se han configurado + if "guaranteed-bw-percent" not in qos_class.get("bandwidth", {}): + qos_class.setdefault("bandwidth", {})["guaranteed-bw-percent"] = 0 + if "svc-mtu" not in service: + service["svc-mtu"] = 1500 \ No newline at end of file diff --git a/src/realizer/restconf/service_types/builders/create_network_access.py b/src/realizer/restconf/service_types/builders/create_network_access.py index 62fd3f5..c4725b5 100644 --- a/src/realizer/restconf/service_types/builders/create_network_access.py +++ b/src/realizer/restconf/service_types/builders/create_network_access.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -23,15 +23,17 @@ def create_network_access(sdp, ietf_intent, connectivity_type, router_id, router if layer_type == "l2": access_id = "network-access-id" access_type = "type" + direction = "bidirectional" elif layer_type == "l3": access_id = "site-network-access-id" access_type = "site-network-access-type" + direction = "both" ip_connection = { "ipv4": { - "address-allocation-type": "ietf-l3vpn-svc:static-address", + "address-allocation-type": "static-address", "addresses": { "provider-address": safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-address"]), - "customer-address": "", + # "customer-address": "", "prefix-length": safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-prefix-length"]) } } @@ -39,11 +41,11 @@ def create_network_access(sdp, ietf_intent, connectivity_type, router_id, router network_access = { access_id: router_if, - access_type: f"ietf-{layer_type}-svc:{connectivity_type}", + access_type: f"{connectivity_type}", "device-reference": router_id, "vpn-attachment": { "vpn-id": ietf_intent["id"], - "site-role": f"ietf-{layer_type}-svc:{sdp['type']}" + # "site-role": f"{sdp['type']}" # Revisar esto!!!! }, "service": { "qos": { @@ -52,7 +54,7 @@ def create_network_access(sdp, ietf_intent, connectivity_type, router_id, router "classes": { "class": [{ "class-id": "qos-realtime", - "direction": f"ietf-{layer_type}-svc:both", + "direction": direction, }] } } diff --git a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py index b0b188e..0053f0b 100644 --- a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py +++ b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -51,6 +51,9 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): "location": location }] }, + "management": { + "type": "provider-managed" + }, "site-network-accesses": { "site-network-access": [create_network_access(sdp, ietf_intent, connectivity_type, router_id, router_if, layer_type)] } diff --git a/src/realizer/restconf/service_types/builders/initialize_structure.py b/src/realizer/restconf/service_types/builders/initialize_structure.py index f32a8ff..7d81f64 100644 --- a/src/realizer/restconf/service_types/builders/initialize_structure.py +++ b/src/realizer/restconf/service_types/builders/initialize_structure.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/service_types/l2vpn.py b/src/realizer/restconf/service_types/l2vpn.py index 34fa537..7f17235 100644 --- a/src/realizer/restconf/service_types/l2vpn.py +++ b/src/realizer/restconf/service_types/l2vpn.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/restconf/service_types/l3vpn.py b/src/realizer/restconf/service_types/l3vpn.py index 7ffd560..1e73866 100644 --- a/src/realizer/restconf/service_types/l3vpn.py +++ b/src/realizer/restconf/service_types/l3vpn.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/select_way.py b/src/realizer/select_way.py index 016f9ee..d0f2c58 100644 --- a/src/realizer/select_way.py +++ b/src/realizer/select_way.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/send_controller.py b/src/realizer/send_controller.py index ee757fa..f7ecd24 100644 --- a/src/realizer/send_controller.py +++ b/src/realizer/send_controller.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. @@ -62,7 +62,7 @@ def send_controller(controller_type, requests): elif controller_type == "E2E": response = e2e_connect(requests, current_app.config["TFS_E2E"]) logging.info("Requests sent to Teraflow E2E") - elif controller_type == "restconf": + elif controller_type == "RESTCONF": response = restconf_connect(requests, current_app.config["RESTCONF_IP"]) logging.info("Requests sent to restconf controller") return response \ No newline at end of file diff --git a/src/realizer/tfs/helpers/cisco_connector.py b/src/realizer/tfs/helpers/cisco_connector.py index a1f7444..27a3b9d 100644 --- a/src/realizer/tfs/helpers/cisco_connector.py +++ b/src/realizer/tfs/helpers/cisco_connector.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/tfs/helpers/tfs_connector.py b/src/realizer/tfs/helpers/tfs_connector.py index 91c64f4..a4f2139 100644 --- a/src/realizer/tfs/helpers/tfs_connector.py +++ b/src/realizer/tfs/helpers/tfs_connector.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/tfs/main.py b/src/realizer/tfs/main.py index 74be42a..8e98a7b 100644 --- a/src/realizer/tfs/main.py +++ b/src/realizer/tfs/main.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/tfs/service_types/tfs_l2vpn.py b/src/realizer/tfs/service_types/tfs_l2vpn.py index a70fcf8..e2a9a66 100644 --- a/src/realizer/tfs/service_types/tfs_l2vpn.py +++ b/src/realizer/tfs/service_types/tfs_l2vpn.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/tfs/service_types/tfs_l3vpn.py b/src/realizer/tfs/service_types/tfs_l3vpn.py index 684d191..5d561d4 100644 --- a/src/realizer/tfs/service_types/tfs_l3vpn.py +++ b/src/realizer/tfs/service_types/tfs_l3vpn.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/realizer/tfs/tfs_connect.py b/src/realizer/tfs/tfs_connect.py index 9c8334d..99697a1 100644 --- a/src/realizer/tfs/tfs_connect.py +++ b/src/realizer/tfs/tfs_connect.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 9a1e5b2..353198f 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + import json import pytest import os diff --git a/src/tests/test_database.py b/src/tests/test_database.py index 06034eb..9ca92c3 100644 --- a/src/tests/test_database.py +++ b/src/tests/test_database.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 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 pytest import sqlite3 import json diff --git a/src/tests/test_e2e.py b/src/tests/test_e2e.py index 9fb9140..6e9f2e4 100644 --- a/src/tests/test_e2e.py +++ b/src/tests/test_e2e.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 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 pytest import json from pathlib import Path diff --git a/src/tests/test_initialization.py b/src/tests/test_initialization.py index c51cc06..3500465 100644 --- a/src/tests/test_initialization.py +++ b/src/tests/test_initialization.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 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 pytest # Importa tu clase (ajusta el nombre del módulo si es distinto) diff --git a/src/tests/test_mapper.py b/src/tests/test_mapper.py index 076caab..3dfd1ec 100644 --- a/src/tests/test_mapper.py +++ b/src/tests/test_mapper.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 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 pytest import logging from unittest.mock import patch, MagicMock, call diff --git a/src/tests/test_nbi_processor.py b/src/tests/test_nbi_processor.py index 8626213..40db65e 100644 --- a/src/tests/test_nbi_processor.py +++ b/src/tests/test_nbi_processor.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 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 pytest from unittest.mock import patch from src.nbi_processor.detect_format import detect_format diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py index f79526d..64d26bf 100644 --- a/src/tests/test_utils.py +++ b/src/tests/test_utils.py @@ -1,3 +1,19 @@ +# Copyright 2022-2026 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + import json import pytest import os diff --git a/src/utils/build_response.py b/src/utils/build_response.py index da857ab..a3da824 100644 --- a/src/utils/build_response.py +++ b/src/utils/build_response.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/utils/dump_templates.py b/src/utils/dump_templates.py index f3fbb44..2c516fd 100644 --- a/src/utils/dump_templates.py +++ b/src/utils/dump_templates.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/utils/load_template.py b/src/utils/load_template.py index fd2cd8b..71c16b8 100644 --- a/src/utils/load_template.py +++ b/src/utils/load_template.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/utils/safe_get.py b/src/utils/safe_get.py index c02b1bf..7fad433 100644 --- a/src/utils/safe_get.py +++ b/src/utils/safe_get.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/utils/send_response.py b/src/utils/send_response.py index 30c4433..35b4016 100644 --- a/src/utils/send_response.py +++ b/src/utils/send_response.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/utils/slice_manager.py b/src/utils/slice_manager.py index 17e01bb..9581817 100644 --- a/src/utils/slice_manager.py +++ b/src/utils/slice_manager.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/webui/gui.py b/src/webui/gui.py index e2d340b..73db9f1 100644 --- a/src/webui/gui.py +++ b/src/webui/gui.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/src/webui/templates/dev.html b/src/webui/templates/dev.html index 7a18ecc..da37fdd 100644 --- a/src/webui/templates/dev.html +++ b/src/webui/templates/dev.html @@ -1,4 +1,4 @@ - + diff --git a/src/webui/templates/index.html b/src/webui/templates/index.html index bef6b7a..43dbcbc 100644 --- a/src/webui/templates/index.html +++ b/src/webui/templates/index.html @@ -1,4 +1,4 @@ - + diff --git a/src/webui/templates/ixia.html b/src/webui/templates/ixia.html index f9821e8..8712d27 100644 --- a/src/webui/templates/ixia.html +++ b/src/webui/templates/ixia.html @@ -1,4 +1,4 @@ - + diff --git a/src/webui/templates/login.html b/src/webui/templates/login.html index 5dd54b6..e3796df 100644 --- a/src/webui/templates/login.html +++ b/src/webui/templates/login.html @@ -1,4 +1,4 @@ - + diff --git a/src/webui/templates/search.html b/src/webui/templates/search.html index 6ace2c1..f2432ce 100644 --- a/src/webui/templates/search.html +++ b/src/webui/templates/search.html @@ -1,4 +1,4 @@ - + diff --git a/src/webui/templates/welcome.html b/src/webui/templates/welcome.html index 3fa27b2..cea390f 100644 --- a/src/webui/templates/welcome.html +++ b/src/webui/templates/welcome.html @@ -1,4 +1,4 @@ - + diff --git a/swagger/E2E_namespace.py b/swagger/E2E_namespace.py index a53cd0d..72a40b7 100644 --- a/swagger/E2E_namespace.py +++ b/swagger/E2E_namespace.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (E2E) (https://E2E.etsi.org/) +# Copyright 2022-2026 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. diff --git a/swagger/ixia_namespace.py b/swagger/ixia_namespace.py index e8f4ac9..12f122b 100644 --- a/swagger/ixia_namespace.py +++ b/swagger/ixia_namespace.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/swagger/models/create_models.py b/swagger/models/create_models.py index 5694d78..6c2a3f9 100644 --- a/swagger/models/create_models.py +++ b/swagger/models/create_models.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/swagger/models/create_models_restconf.py b/swagger/models/create_models_restconf.py index c05cb17..0a4c0ae 100644 --- a/swagger/models/create_models_restconf.py +++ b/swagger/models/create_models_restconf.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/swagger/restconf_namespace.py b/swagger/restconf_namespace.py index bd758b5..aa7e18a 100644 --- a/swagger/restconf_namespace.py +++ b/swagger/restconf_namespace.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. diff --git a/swagger/tfs_namespace.py b/swagger/tfs_namespace.py index 0916360..2686b83 100644 --- a/swagger/tfs_namespace.py +++ b/swagger/tfs_namespace.py @@ -1,4 +1,4 @@ -# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2026 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. -- GitLab From b3be306d2b38943b7f6291f55cc2dd5384f01d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Wed, 6 May 2026 07:45:35 +0000 Subject: [PATCH 05/27] Delete copyright headers script log files --- out-no-header.txt | 16 -- out-skipped.txt | 381 ---------------------------------------------- out-updated.txt | 88 ----------- 3 files changed, 485 deletions(-) delete mode 100644 out-no-header.txt delete mode 100644 out-skipped.txt delete mode 100644 out-updated.txt diff --git a/out-no-header.txt b/out-no-header.txt deleted file mode 100644 index 03c635a..0000000 --- a/out-no-header.txt +++ /dev/null @@ -1,16 +0,0 @@ -./slice.db -./yang-modules/ietf-ac-common@2025-09-29.yang -./yang-modules/ietf-key-chain@2017-06-15.yang -./yang-modules/ietf-routing-types@2017-12-04.yang -./yang-modules/ietf-network@2018-02-26.yang -./yang-modules/ietf-ethertypes@2019-03-04.yang -./yang-modules/ietf-netconf-acm@2018-02-14.yang -./yang-modules/ietf-vpn-common@2022-02-11.yang -./yang-modules/ietf-te-types@2025-10-17.yang -./yang-modules/ietf-geo-location@2022-02-11.yang -./yang-modules/ietf-ac-svc@2025-09-29.yang -./yang-modules/ietf-packet-fields@2019-03-04.yang -./yang-modules/ietf-te-packet-types@2025-01-24.yang -./yang-modules/tree.txt -./yang-modules/ietf-network-topology@2018-02-26.yang -./yang-modules/ietf-network-slice-service@2025-05-09.yang diff --git a/out-skipped.txt b/out-skipped.txt deleted file mode 100644 index 9a01149..0000000 --- a/out-skipped.txt +++ /dev/null @@ -1,381 +0,0 @@ -./.env -./out-skipped.txt -./README.md -./LICENSE -./out-updated.txt -./out-no-header.txt -./images/NSC_Architecture.png -./src/mapper/__pycache__/main.cpython-312.pyc -./src/mapper/__pycache__/slo_viability.cpython-312.pyc -./src/mapper/__pycache__/slo_viability.cpython-39.pyc -./src/mapper/__pycache__/main.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-312.pyc -./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-312.pyc -./src/realizer/ixia/__pycache__/main.cpython-312.pyc -./src/realizer/ixia/__pycache__/ixia_connect.cpython-312.pyc -./src/realizer/ixia/__pycache__/ixia_connect.cpython-39.pyc -./src/realizer/ixia/__pycache__/main.cpython-39.pyc -./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-312.pyc -./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-312.pyc -./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-39.pyc -./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-39.pyc -./src/realizer/e2e/__pycache__/e2e_connect.cpython-39.pyc -./src/realizer/e2e/__pycache__/main.cpython-312.pyc -./src/realizer/e2e/__pycache__/e2e_connect.cpython-312.pyc -./src/realizer/e2e/__pycache__/main.cpython-39.pyc -./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-312.pyc -./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-39.pyc -./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-312.pyc -./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-39.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-312.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-39.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-312.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-39.pyc -./src/realizer/tfs/__pycache__/main.cpython-312.pyc -./src/realizer/tfs/__pycache__/tfs_connect.cpython-39.pyc -./src/realizer/tfs/__pycache__/tfs_connect.cpython-312.pyc -./src/realizer/tfs/__pycache__/main.cpython-39.pyc -./src/realizer/__pycache__/send_controller.cpython-39.pyc -./src/realizer/__pycache__/main.cpython-312.pyc -./src/realizer/__pycache__/send_controller.cpython-312.pyc -./src/realizer/__pycache__/select_way.cpython-39.pyc -./src/realizer/__pycache__/select_way.cpython-312.pyc -./src/realizer/__pycache__/nrp_handler.cpython-312.pyc -./src/realizer/__pycache__/nrp_handler.cpython-39.pyc -./src/realizer/__pycache__/main.cpython-39.pyc -./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-312.pyc -./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-39.pyc -./src/planner/hrat_planner/__pycache__/hrat.cpython-312.pyc -./src/planner/hrat_planner/__pycache__/hrat.cpython-39.pyc -./src/planner/__pycache__/planner.cpython-39.pyc -./src/planner/__pycache__/planner.cpython-312.pyc -./src/planner/energy_planner/topo_ddbb.json -./src/planner/energy_planner/energy_ddbb.json -./src/planner/energy_planner/ext_topo_ddbb.json -./src/planner/energy_planner/__pycache__/energy.cpython-39.pyc -./src/planner/energy_planner/__pycache__/energy.cpython-312.pyc -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_1.json -./src/tests/requests/3gpp_template_UC1PoC2_midhaul.json -./src/tests/requests/l3vpn_test.json -./src/tests/requests/ietf_green_request.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul.json -./src/tests/requests/frr_request.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_2.json -./src/tests/requests/create_slice_1.json -./src/tests/requests/ietf_template_timing.json -./src/tests/requests/slice_request.json -./src/tests/requests/P2MP.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_3.json -./src/tests/requests/3ggpp_template_green.json -./src/config/IPs.json -./src/config/__pycache__/config.cpython-39.pyc -./src/config/__pycache__/constants.cpython-39.pyc -./src/config/__pycache__/config.cpython-312.pyc -./src/config/__pycache__/constants.cpython-312.pyc -./src/database/nrp_ddbb.json -./src/database/__pycache__/db.cpython-312.pyc -./src/database/__pycache__/sysrepo_store.cpython-39.pyc -./src/database/__pycache__/store_data.cpython-312.pyc -./src/database/__pycache__/db.cpython-39.pyc -./src/database/__pycache__/store_data.cpython-39.pyc -./src/database/__pycache__/sysrepo_store.cpython-312.pyc -./src/templates/ietfL3VPN_template_empty.json -./src/templates/L3-VPN_template_empty.json -./src/templates/Optical_slice.json -./src/templates/3gpp_template_empty.json -./src/templates/ietf_template_empty.json -./src/templates/TAPI_service.json -./src/templates/L2-VPN_template_empty.json -./src/templates/ietfL2VPN_template_empty.json -./src/templates/IPoWDM_orchestrator.json -./src/nbi_processor/__pycache__/main.cpython-312.pyc -./src/nbi_processor/__pycache__/translator.cpython-312.pyc -./src/nbi_processor/__pycache__/translator.cpython-39.pyc -./src/nbi_processor/__pycache__/detect_format.cpython-312.pyc -./src/nbi_processor/__pycache__/main.cpython-39.pyc -./src/nbi_processor/__pycache__/detect_format.cpython-39.pyc -./src/api/__pycache__/main.cpython-312.pyc -./src/api/__pycache__/main.cpython-39.pyc -./src/__pycache__/main.cpython-312.pyc -./src/__pycache__/main.cpython-39.pyc -./src/webui/static/img/MiniLogo.png -./src/webui/static/img/TelefonicaLogo.png -./src/webui/static/img/banner.png -./src/webui/__pycache__/gui.cpython-39.pyc -./src/webui/__pycache__/gui.cpython-312.pyc -./src/utils/__pycache__/dump_templates.cpython-39.pyc -./src/utils/__pycache__/safe_get.cpython-39.pyc -./src/utils/__pycache__/safe_get.cpython-312.pyc -./src/utils/__pycache__/build_response.cpython-312.pyc -./src/utils/__pycache__/load_template.cpython-312.pyc -./src/utils/__pycache__/send_response.cpython-312.pyc -./src/utils/__pycache__/dump_templates.cpython-312.pyc -./src/utils/__pycache__/build_response.cpython-39.pyc -./src/utils/__pycache__/load_template.cpython-39.pyc -./src/utils/__pycache__/send_response.cpython-39.pyc -./swagger/restconf_swagger_full.json -./swagger/restconf_swagger.json -./swagger/models/__pycache__/create_models.cpython-39.pyc -./swagger/models/__pycache__/create_models.cpython-312.pyc -./swagger/models/__pycache__/create_models_restconf.cpython-312.pyc -./swagger/__pycache__/E2E_namespace.cpython-39.pyc -./swagger/__pycache__/ixia_namespace.cpython-312.pyc -./swagger/__pycache__/tfs_namespace.cpython-312.pyc -./swagger/__pycache__/restconf_namespace.cpython-312.pyc -./swagger/__pycache__/tfs_namespace.cpython-39.pyc -./swagger/__pycache__/E2E_namespace.cpython-312.pyc -./swagger/__pycache__/ixia_namespace.cpython-39.pyc -./.gitlab/issue_templates/enhancement.md -./.gitlab/issue_templates/new-feature.md -./.gitlab/issue_templates/bug.md -./.gitlab/issue_templates/new-feature-with-design.md -./.git/index -./.git/HEAD -./.git/ORIG_HEAD -./.git/FETCH_HEAD -./.git/description -./.git/config -./.git/packed-refs -./.git/COMMIT_EDITMSG -./.git/objects/a1/c55879cb8c020c875f9237f726c4cf110297aa -./.git/objects/24/319c155fb104e20bee79e5b257317b01323197 -./.git/objects/d3/a1c91438954552d86907f8e2ab41a0cdf0c8e4 -./.git/objects/d9/da81eeebefad9054a43af552de3d51f20bcd56 -./.git/objects/bc/0bbade1bca1444abc8e4f834a7d0e3ad714bcc -./.git/objects/83/b8d3d4ad4e28b52e88d38a769f56109b5b8e54 -./.git/objects/13/364ab58163d44a3e6bbfbf25d7cb3b0f484e98 -./.git/objects/13/0ef01e5b6b9388458379a3602b1ec98374bc31 -./.git/objects/84/ffcc16d441fee227dd11117d87e2945cbb85d7 -./.git/objects/56/94d78e71d85b1da4eff3ad3a45af3acf808d9f -./.git/objects/56/930c81de3639098c8c42a511fafdb154c7b26e -./.git/objects/56/a52ec183ae5731cd400b47f3892bc3cdd93f6b -./.git/objects/ce/5604665f56b58b25593deeaf3c38db07db9ef3 -./.git/objects/c7/2736f92d46a5befe01e19a7a8d3049966952c8 -./.git/objects/68/4d191ecb38ed786b6be8faa19b58b1fc684c9b -./.git/objects/9a/50709c0681b663042d652f067c4929e7d61d4c -./.git/objects/9a/1e5b2c267e9226e3af34f8b2047bb9b38018ad -./.git/objects/b2/015d83dadd94e23940447d942d1d8ea6398ad3 -./.git/objects/2a/a4667a5e3db8e6de820394831a54d36fae96ed -./.git/objects/2a/62635019070c7a6edb0988ddc3216f844336ac -./.git/objects/2a/369c6bd4be9badb160d4bdee49cfb5ea605e0b -./.git/objects/30/623e9b710b9f32a386a0c1ff693d322a8cba88 -./.git/objects/ba/cdaf84f155b43df2b89ef0e4efc3d57b0d7fff -./.git/objects/0c/6e465d74277bed20db3cbbb8d6bc9e9c041e65 -./.git/objects/29/5a0d32051bb651cc0dcdd6fefae0468094a7c6 -./.git/objects/1e/c944d791db1da1b8236c6069f10d65b1b6f97f -./.git/objects/1e/6bf8411348d038dc8ab93114c0ec787f2fac1f -./.git/objects/1e/8cf0817554ec18499a53d9c401a3f54d6d49e5 -./.git/objects/c9/ae55eb708854fd67b73410a4f8715e64b3aa57 -./.git/objects/6c/ea7d3642dab6ba8ac09f3e44a07d6591ba99f4 -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.idx -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.rev -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.idx -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.pack -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.idx -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.pack -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.rev -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.pack -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.idx -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.rev -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.pack -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.rev -./.git/objects/a9/1024508763398543610cea7b8e16fa952d28b7 -./.git/objects/a9/cd1d4586eb8befff4a7e90df781eb7806aed39 -./.git/objects/f8/0e69f3bdf2ec564923cfaba9b88ac70882b77c -./.git/objects/f8/00d42dbcc2f59c613d1007c4738e2acb0355d2 -./.git/objects/8f/37e643276bf994d955e3dad95a4cb93c258f67 -./.git/objects/8f/132c895e2946db20e93bd54149330ac0226bfb -./.git/objects/17/5b00798abd846f9413b56e1d3de4937f832684 -./.git/objects/17/eb5d4435e7efcc94acf8e32506c0705cedb8e5 -./.git/objects/80/0c62fe8e5147086dd9f07d03aabaf297a3287f -./.git/objects/e3/8daf12a35eda16070034191c7a2c5220c5e057 -./.git/objects/e3/019e59e794d66493817c9d76c75ba7718ec4df -./.git/objects/da/3ef7a52980a769f2ed10e1e1167995af16576c -./.git/objects/da/857ab54a12b2dbe6315042b7bdfccc1aff19cc -./.git/objects/50/b00fbe85eeaa64bcbe8159c3d04164d3ea9113 -./.git/objects/d1/f11cd63da20457265e6a159531c05b8b0ea75e -./.git/objects/82/b67a1ca7cf7b568cb9fa566a6ab065072aaa2e -./.git/objects/5e/79c73f4c3a8a1be7f0cb84f37d0cf93a9b6da0 -./.git/objects/5e/da6a83ccffc29200838e23f5b7feecbc3aa8ce -./.git/objects/5e/fe8d47ca5f0ec8c2f89d4dbbf91de604da0565 -./.git/objects/7f/24267ce03103755f28099cc124c0a821fd739e -./.git/objects/7f/6f0a8a616fc2eb18dbd69255ff35ef43041931 -./.git/objects/cf/da9cf8b25de8bc8dfbe63f728e5e929e6bd66b -./.git/objects/cf/f183b0d73d0a799af3a09f19583ecc6e780833 -./.git/objects/cf/aa9ee46f37057b0cba026a5d042d1ef3f6438d -./.git/objects/7c/c66d11d7ba06287896a33b2c018c2061e22851 -./.git/objects/87/28250df41f46adc8fa6b412e676c3e0117fa41 -./.git/objects/43/0c261a579e911ee8742490dbc87238a1c1cc08 -./.git/objects/4d/ebe53ee691012be9a33b44b6ee508315db8ca9 -./.git/objects/eb/56ca633390c63c058c351e3c9492b6ef5374f5 -./.git/objects/d2/46c96bd2ff97920f294fb6b0213cc7c70137c2 -./.git/objects/7b/ce196c1667ee6c8f708e674a7f8e20724ecfc0 -./.git/objects/65/d4040710f3628da6fe84d5a538c1675fb1ccb9 -./.git/objects/af/3bafe1661a00bb7cfc9f44157f34af84befa28 -./.git/objects/00/396bbf60369411bfb43b7a84a87f19facc5dee -./.git/objects/45/4eb176fe183a1a1eed5826548cc95f16fc18aa -./.git/objects/45/bb5358f6219afdd12de9058c0917fb8823b4a4 -./.git/objects/9c/e5fa89b675aa014166c81b58e73848aaf6042a -./.git/objects/ca/57941fc5a97914a1727ab42a988a8e8e3467ff -./.git/objects/52/a393230b2fb369d6954208a7504efb830442a9 -./.git/objects/52/d85bce8df704dc8890ddc79ecb6dcfd1295159 -./.git/objects/4b/4ae60160daa6c1e12ab2ad325e130ac62f557f -./.git/objects/8c/e41b9930a05732402ee3d8661f497297b6750f -./.git/objects/8c/ef7eeb08caf1349d4f1cdd3b52b8d01132e151 -./.git/objects/5a/aa083f84abd1c8ca2ffcb7d5c19bd63600611c -./.git/objects/a6/461e6335cfe0b97521db324595cb489fc1c41b -./.git/objects/2f/efbaab653094924acb3b0156c1eab15e752f85 -./.git/objects/2f/b797bd87bf4ed825f83ec788df707b94c5f68b -./.git/objects/e1/f13a86f6084d0906a384b0421385bb829d89d0 -./.git/objects/86/3e3ab7d7f0da019483b0293706569fdb295bc6 -./.git/objects/75/704a402d5653bdeb8b0bc90c011d67f1d0c3cf -./.git/objects/b7/dc48c63a8954330f2f8246e423dfb56a8563f5 -./.git/objects/41/baeecff513727319d7b0f395e822ecfd1fedc4 -./.git/objects/41/a7996ce90a56369c30484b143c931fd18b4518 -./.git/objects/c0/eca0991b29295a0afeaaa239d3b25aa2551d3d -./.git/objects/1a/2966234be8d0a24ffeead6dfb2525f645dcdbf -./.git/objects/1a/650c3076c03324bfdfe6754a125f0e1fa8a145 -./.git/objects/95/2b856ba4244babb1dab96d3698d0f286e073cd -./.git/objects/35/f862c574b53d55da5c4db62e29b305716b5d90 -./.git/objects/fd/055074aeba5c277bbefdce0b81ebd24d0d3551 -./.git/objects/96/5ddb39541fe96b8c71683b4ad5b63a08316daa -./.git/objects/96/418bb720ff04b7a36ee47b45a570692a7278a8 -./.git/objects/c2/9f6a40d317311ce82e756304033d5dd105c390 -./.git/objects/fc/9940922faff4ccccd2eaf705ed94f9982ac047 -./.git/objects/fc/16b40610986249153e18fc9650469671b46b95 -./.git/objects/90/ed7fa37077ddad67e2f82d7881df2f617be577 -./.git/objects/6f/3933c422c188054aeec25d640541348bde5fdd -./.git/objects/6f/7ec43b9fc24a5f9cc2de84422ad4f027fec93d -./.git/objects/60/7f11e8e85f7b241937efdf91a4f43b14e65ab9 -./.git/objects/25/67a613ebdf545a1f1bd650566ba947162efc35 -./.git/objects/36/83a2581ba7d2517a0b5e59edc594893ae6d99c -./.git/objects/36/f8d72d0627516b64420ed0bc1175700f711857 -./.git/objects/51/abd33613baeebbad2202efca9e022c6d55149f -./.git/objects/51/47a8e3ae3343937f77bc56f3b3048988e35404 -./.git/objects/0a/28bb8a469ecce2bacd0e3d57255964cad4e7ca -./.git/objects/2e/ba42828c5ff7ae54dd242beab47700b79b987d -./.git/objects/f5/fcc3572df93b2752b1ed393cb09e0c501f7a15 -./.git/objects/ef/16b4394fee65ab326bee56e80c5069a844992c -./.git/objects/67/be760a1c939c84bb1310a22ff1f35ea9c9abb0 -./.git/objects/8b/e0afbc8d5f2cadc93eb685a4f210aab0458de2 -./.git/objects/33/45a89c51c736f929344b68f897e560fb7798e0 -./.git/objects/33/5b7e601c09e5751a80f346c9aafb93d17101d2 -./.git/objects/72/05a1fdf91869d35795d35df9a44fcf0a365e3f -./.git/objects/c5/59d204968a27b32f5eb3235a91b07317749239 -./.git/objects/93/2d002794afabda8f14c595d1e9644943ca2757 -./.git/objects/44/5d1994a5ac57366078b198200a9e143d4ccda8 -./.git/objects/5f/d71bb7aba1ce2209373b626e99e8b7d6f4c03f -./.git/objects/0d/65d4ea9dbfe0e79f437e9efbd78c56ada25688 -./.git/objects/b9/3f0df8f29b3d8070ed2a9f54bbe31029fe66e1 -./.git/objects/b9/7cd799d77968fdc4cdfef22365468470cf0eb4 -./.git/objects/4f/fb116001eeded5674daef6abef65089208ed1a -./.git/objects/ff/b1e0654e644d404272145c9e82d40c70da3fcf -./.git/objects/fb/df8b0da5f9b93d54bb5f221c3bc97704675004 -./.git/objects/6b/680462719b3b632f1a37f89ce79005df57ef23 -./.git/objects/6b/ffc7308bc85dde17a094c88e4db4c055f72488 -./.git/objects/6b/1c053d37cecbcb581c74be40a7b0a76d450575 -./.git/objects/85/d23814e172addd8c22c0f8fa0bd2f2a62212f5 -./.git/objects/b6/098d3a5d03e998d50c9b87b06c3c1940ee7c4f -./.git/objects/bf/4855faf0508a152471f6c6c8f756581b8ebb96 -./.git/objects/bf/45cf47afbe1af29b37202ffa7aad670f724315 -./.git/objects/c8/2b2f7a810646753627819a01e54b2fbb0bf994 -./.git/objects/fa/64f14a857fee267529d204b1459599cdcd4c1c -./.git/objects/64/76ab8696fde6f30a60496b6432e722be4f7ecb -./.git/objects/64/bc3be12e5a19ba7b8d2feef676f87de847c7ee -./.git/objects/31/9ae2c46027c14596f9cbb2b7feac7638a53ac3 -./.git/objects/63/5ac650b9e5cb5102933f3b31acd333c5a80893 -./.git/objects/63/e6828bfe6daec53b28903951dba03be01d3030 -./.git/objects/79/eb1ce74e5351a6f490e0982cbc6e129b4aa7c8 -./.git/objects/5c/ca0f79b82611f512e6294a0f00596a234eb85c -./.git/objects/b3/16e45895dfd681cc04ee3301c329a74fdba090 -./.git/objects/1f/8147d384285cfe5ff18e96ebeff64aca142ab2 -./.git/objects/3b/4c4f812a8d80d92927085740fee2bafe5a602f -./.git/objects/23/16ac2bb4ceac9df62c1ff1e1a8a7bdcb01e5d7 -./.git/objects/9b/3926088eae0e151326f6f0015829521df010b4 -./.git/objects/27/8591987f33b59eb256926e6d9c96a0cdb40b92 -./.git/objects/d5/a0cd040e7aba6e44e5cb6c48b3b31de7fa3996 -./.git/objects/d5/61671aaa8c1913d335a6750e9fad59f6065448 -./.git/objects/d5/d06687674c7ac43f59839c7d16ffe001f7d0c7 -./.git/objects/d5/8b47cc8995eda0d0902699f1037849b620e723 -./.git/objects/d5/4fbe58d8f2468a576a9cec2bcaaa26801f6b3d -./.git/objects/d5/45ab9b4098ec8c1818861356b9fe334552af1a -./.git/objects/fe/ea2538a9529bd851c99b976099e226409080c3 -./.git/objects/fe/4849b2f3a88b6f3997f565ad7d9da97c402a58 -./.git/objects/fe/b44f5214740a873ac5eedbb942834d8916e07d -./.git/objects/55/3aa665e46a83e53e331b2435e4eacd6e696ba0 -./.git/objects/55/b6eb9fc430d168b02cbf5a231d93bb7632ab31 -./.git/objects/a0/e4208fa05edf37e2f64955495172d860048b74 -./.git/objects/7e/db6adeb6c45b5b3524c1c33d2c1abdfd2ef4b3 -./.git/objects/c4/507da960010d98b9560a34fe55686858f1116b -./.git/objects/38/a157dfa5222bb8aad0c3531a07c0eb288a9f0a -./.git/objects/b8/15446f819ac2dcba50e63e5781e8a1be9b59c6 -./.git/objects/74/d094765ec06d081a8eb0cf62a2e6f3812829a0 -./.git/objects/d4/7bcee05a434c6b87726710fcc854971a72d606 -./.git/objects/09/cad2ec21e916c54b3aebb07431df19760c0e25 -./.git/objects/a4/a10b59d02c8c64af41d0bb0255873e888ea89d -./.git/objects/e0/1a219d20f0a4e2a6fb4747565f072e89dbe3ee -./.git/objects/53/27cdf631d950ea1e10d0517bdcd67179e9721f -./.git/objects/37/30fb786f1b523cd179652a96f821ce88eea36d -./.git/objects/37/650c30d77f69b0b011040293e053a4e7c6f98c -./.git/objects/28/f953b838d0200c67301cc7f3a0d352e26f3c42 -./.git/objects/f2/d19117ef0cd800c52fde833c32acb71e46a664 -./.git/objects/4c/5dd0a4298e91212e5422fa71254fc0e5176158 -./.git/objects/4c/be96ff54343b2f28a2b5069343ec49a4342ba9 -./.git/objects/a8/d605fb18e5c5ee1f3f5352c1460e1f760535bd -./.git/objects/a8/d7d38e73f05bf78472f59a092bdef3949483e7 -./.git/objects/b0/165037a79df96cdc3570f14250ae79bb3e693e -./.git/objects/b0/af64a3d1d8dc88b08bb9208dea14f8761f973f -./.git/objects/2b/38518e1f2a3349a23737edb17748c0ec1428cc -./.git/objects/2b/52a895406f2999f2fed4bbb21dcd33d7569d6a -./.git/objects/b4/80245fb3f89a446b36d21c8b1b6df792e2e5a8 -./.git/objects/7a/6d78346ba3078860f945a4f56f88a8e50d2b42 -./.git/objects/58/f8841a3fc96b9e14be040842de44b85e5276cc -./.git/objects/b1/8e4237f7f9851fbf4180cd4d0549497c165637 -./.git/objects/b1/afe94b6b0a3bd565aabc05acd34e487b914071 -./.git/objects/c1/df659fb1b5539c087fa05da398c1ffa81712b8 -./.git/objects/c1/eb94b7d6938b2ec524f98d5b6ea9276139d26d -./.git/objects/c1/b79b5e4a0d723e1c8a437c21bba6846b39a9c8 -./.git/objects/81/3380a694d4f4961a9feea10d881c3554d41155 -./.git/objects/81/cedd6d7915b885fefd382950534d4d5ee4d2de -./.git/objects/e2/d340bf4d50c22789fcae8145de3d53e9487bc8 -./.git/objects/e2/2ce8b7aa5354f50987e9646e7b20274d7d8f3f -./.git/objects/e2/6091b8dfab7cd3efd7f0ce8f7b1dc8bd540f6f -./.git/objects/9e/73b99fe785e2c18588575ebea9e63409e6390b -./.git/objects/34/3455561e55393181b9ad342eedb62b242a59ca -./.git/objects/2d/9823652e7ca859d5f826b475c4d7a9bc979919 -./.git/logs/HEAD -./.git/logs/refs/heads/main -./.git/logs/refs/heads/develop -./.git/logs/refs/heads/feat/25-tid-improve-mapping-of-slice-requests -./.git/logs/refs/heads/feat/24-tid-restconf-base-nbi -./.git/logs/refs/remotes/origin/sat_module -./.git/logs/refs/remotes/origin/HEAD -./.git/logs/refs/remotes/origin/develop -./.git/logs/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests -./.git/logs/refs/remotes/origin/feat/24-tid-restconf-base-nbi -./.git/refs/heads/main -./.git/refs/heads/develop -./.git/refs/heads/feat/25-tid-improve-mapping-of-slice-requests -./.git/refs/heads/feat/24-tid-restconf-base-nbi -./.git/refs/remotes/origin/sat_module -./.git/refs/remotes/origin/HEAD -./.git/refs/remotes/origin/develop -./.git/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests -./.git/refs/remotes/origin/feat/24-tid-restconf-base-nbi -./.git/info/exclude -./.git/hooks/post-update.sample -./.git/hooks/pre-push.sample -./.git/hooks/sendemail-validate.sample -./.git/hooks/pre-receive.sample -./.git/hooks/pre-merge-commit.sample -./.git/hooks/pre-rebase.sample -./.git/hooks/update.sample -./.git/hooks/pre-applypatch.sample -./.git/hooks/fsmonitor-watchman.sample -./.git/hooks/push-to-checkout.sample -./.git/hooks/applypatch-msg.sample -./.git/hooks/pre-commit.sample -./.git/hooks/commit-msg.sample -./.git/hooks/prepare-commit-msg.sample diff --git a/out-updated.txt b/out-updated.txt deleted file mode 100644 index 6953feb..0000000 --- a/out-updated.txt +++ /dev/null @@ -1,88 +0,0 @@ -./Dockerfile -./.gitignore -./app.py -./deploy.sh -./.dockerignore -./.gitlab-ci.yml -./requirements.txt -./src/main.py -./src/mapper/process_connnectivity.py -./src/mapper/main.py -./src/mapper/get_service_template.py -./src/mapper/get_template.py -./src/mapper/slo_viability.py -./src/mapper/extract_sdp_info.py -./src/realizer/nrp_handler.py -./src/realizer/main.py -./src/realizer/send_controller.py -./src/realizer/select_way.py -./src/realizer/ixia/ixia_connect.py -./src/realizer/ixia/main.py -./src/realizer/ixia/helpers/automatizacion_ne2v4.py -./src/realizer/ixia/helpers/NEII_V4.py -./src/realizer/restconf/restconf_connect.py -./src/realizer/restconf/main.py -./src/realizer/restconf/connectors/tfs_connector.py -./src/realizer/restconf/connectors/frr_connector.py -./src/realizer/restconf/connectors/cisco_connector.py -./src/realizer/restconf/service_types/l3vpn.py -./src/realizer/restconf/service_types/l2vpn.py -./src/realizer/restconf/service_types/builders/configure_match_criteria.py -./src/realizer/restconf/service_types/builders/create_network_access.py -./src/realizer/restconf/service_types/builders/configure_slos.py -./src/realizer/restconf/service_types/builders/create_site_from_sdp.py -./src/realizer/restconf/service_types/builders/initialize_structure.py -./src/realizer/restconf/service_types/builders/apply_metric_constraint.py -./src/realizer/e2e/e2e_connect.py -./src/realizer/e2e/main.py -./src/realizer/e2e/service_types/del_l3ipowdm_slice.py -./src/realizer/e2e/service_types/l3ipowdm_slice.py -./src/realizer/tfs/tfs_connect.py -./src/realizer/tfs/main.py -./src/realizer/tfs/helpers/tfs_connector.py -./src/realizer/tfs/helpers/cisco_connector.py -./src/realizer/tfs/service_types/tfs_l2vpn.py -./src/realizer/tfs/service_types/tfs_l3vpn.py -./src/planner/planner.py -./src/planner/tfs_optical_planner/tfs_optical.py -./src/planner/hrat_planner/hrat.py -./src/planner/energy_planner/energy.py -./src/tests/test_e2e.py -./src/tests/test_mapper.py -./src/tests/test_api.py -./src/tests/test_utils.py -./src/tests/test_nbi_processor.py -./src/tests/test_database.py -./src/tests/test_initialization.py -./src/config/config.py -./src/config/.env.example -./src/config/constants.py -./src/database/store_data.py -./src/database/db.py -./src/database/sysrepo_store.py -./src/nbi_processor/detect_format.py -./src/nbi_processor/main.py -./src/nbi_processor/translator.py -./src/api/main.py -./src/webui/gui.py -./src/webui/templates/dev.html -./src/webui/templates/search.html -./src/webui/templates/login.html -./src/webui/templates/index.html -./src/webui/templates/welcome.html -./src/webui/templates/ixia.html -./src/utils/build_response.py -./src/utils/send_response.py -./src/utils/load_template.py -./src/utils/safe_get.py -./src/utils/dump_templates.py -./src/utils/slice_manager.py -./swagger/tfs_namespace.py -./swagger/ixia_namespace.py -./swagger/restconf_namespace.py -./swagger/E2E_namespace.py -./swagger/models/create_models_restconf.py -./swagger/models/create_models.py -./scripts/update_license_headers.py -./scripts/dump_templates_nsc.sh -./scripts/show_logs_nsc.sh -- GitLab From d7042277efe565cd8f43eca8a3bfa4bf65e54dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Wed, 6 May 2026 15:58:06 +0000 Subject: [PATCH 06/27] L3VPN Add routing protocols and vlan match Network access: change custommer address, site role and access type L2VPN Change initial structure --- .../builders/apply_metric_constraint.py | 4 +- .../builders/configure_match_criteria.py | 40 +++++++++++++++---- .../builders/create_network_access.py | 25 ++++++++---- .../builders/create_site_from_sdp.py | 13 +++++- .../builders/initialize_structure.py | 7 ++-- 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/src/realizer/restconf/service_types/builders/apply_metric_constraint.py b/src/realizer/restconf/service_types/builders/apply_metric_constraint.py index 06f799e..f98ec94 100644 --- a/src/realizer/restconf/service_types/builders/apply_metric_constraint.py +++ b/src/realizer/restconf/service_types/builders/apply_metric_constraint.py @@ -31,14 +31,14 @@ def apply_metric_constraint(service, qos_class, constraint, vpn_id, layer_type): "direction": "input-bw", "vpn-id": vpn_id, "cir": bandwidth, - "cbs": bandwidth*0.05 + "cbs": int(bandwidth*0.05) }, { "type": "bw-per-svc", "direction": "output-bw", "vpn-id": vpn_id, "cir": bandwidth, - "cbs": bandwidth*0.05 + "cbs": int(bandwidth*0.05) }, ] } diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index ffff478..5c0788e 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -18,7 +18,7 @@ import logging from src.utils.safe_get import safe_get -def configure_match_criteria(network_access, sdp, layer_type): +def configure_match_criteria(network_access, site, sdp, layer_type): """Configura los criterios de coincidencia en el acceso a la red.""" MATCH_TYPE_MAPPING = { "dscp": "dscp" @@ -26,6 +26,7 @@ def configure_match_criteria(network_access, sdp, layer_type): if layer_type == "l3": MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" + MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: @@ -41,11 +42,34 @@ def configure_match_criteria(network_access, sdp, layer_type): logging.warning(f"Unknown match type: {match_type}") return - rule = { - "id": f"match-{match_type}-{index}", - "match-flow": { - MATCH_TYPE_MAPPING[match_type]: value + provider_address = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-address"]) + prefix_length = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-prefix-length"]) + lan = f"{provider_address}/{prefix_length}" if provider_address and prefix_length else None + + if match_type != "vlan": + rule = { + "id": f"match-{match_type}-{index}", + "match-flow": { + MATCH_TYPE_MAPPING[match_type]: value + } } - } - - network_access["service"]["qos"]["qos-classification-policy"]["rule"].append(rule) \ No newline at end of file + + network_access["service"]["qos"]["qos-classification-policy"]["rule"].append(rule) + else: + # Handle VLAN match criteria + routing_protocol = { + "type": "static", + "static":{ + "cascaded-lan-prefixes": { + "ipv4-lan-prefixes":[ + { + "lan": lan, + "lan-tag": value, + "next-hop": provider_address # This is not correct, should be the management ip of the provider router, but we don't have that info in the SDP. Need to check how to handle this. + } + ] + } + } + } + + site["routing-protocols"]["routing-protocol"].append(routing_protocol) diff --git a/src/realizer/restconf/service_types/builders/create_network_access.py b/src/realizer/restconf/service_types/builders/create_network_access.py index c4725b5..0876f50 100644 --- a/src/realizer/restconf/service_types/builders/create_network_access.py +++ b/src/realizer/restconf/service_types/builders/create_network_access.py @@ -33,19 +33,27 @@ def create_network_access(sdp, ietf_intent, connectivity_type, router_id, router "address-allocation-type": "static-address", "addresses": { "provider-address": safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-address"]), - # "customer-address": "", + "customer-address": safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-address"]), # We set the same address for provider and customer because we don't have the customer address in the SDP. This is not correct and should be fixed in the future when we have that info. "prefix-length": safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-prefix-length"]) } } } + if sdp['type'] == "sender": + site_role = "hub-role" + elif sdp['type'] == "receiver": + site_role = "spoke-role" + else: + site_role = "any-to-any-role" + network_access = { access_id: router_if, - access_type: f"{connectivity_type}", + # access_type: f"{connectivity_type}", + access_type: "multipoint", # We set to multipoint to avoid errors in Teraflow "device-reference": router_id, "vpn-attachment": { "vpn-id": ietf_intent["id"], - # "site-role": f"{sdp['type']}" # Revisar esto!!!! + "site-role": site_role # This will be changed in the future. Hub and spoke roles are only for multipoint connectivity constructs, there is no defined roles por point to point constructs }, "service": { "qos": { @@ -63,9 +71,12 @@ def create_network_access(sdp, ietf_intent, connectivity_type, router_id, router } if layer_type == "l3": network_access["ip-connection"] = ip_connection - - # Configurar match criteria y SLOs - configure_match_criteria(network_access, sdp, layer_type) - configure_slos(network_access, ietf_intent, layer_type) + elif layer_type == "l2": # This should not be needed, but we add it because TFS requires it + network_access["connection"] = { + "oam": { + "md-name": "test", + "md-level": 0 + } + } return network_access diff --git a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py index 0053f0b..5f05f42 100644 --- a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py +++ b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py @@ -17,6 +17,8 @@ import logging from src.utils.safe_get import safe_get from .create_network_access import create_network_access +from .configure_match_criteria import configure_match_criteria +from .configure_slos import configure_slos def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): """ @@ -39,6 +41,8 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): logging.debug(f"Configured site for SDP {safe_get(sdp, ['sdp', 'id'])} with location: {location}, router_id: {router_id}, router_if: {router_if}") + network_access = create_network_access(sdp, ietf_intent, connectivity_type, router_id, router_if, layer_type) + # Crear estructura del site site = { "site-id": safe_get(sdp, ["sdp", "id"]), @@ -55,8 +59,15 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): "type": "provider-managed" }, "site-network-accesses": { - "site-network-access": [create_network_access(sdp, ietf_intent, connectivity_type, router_id, router_if, layer_type)] + "site-network-access": [network_access] } } + + if layer_type == "l3": + site["routing-protocols"] = {"routing-protocol": []} + + # Configure match criteria and SLOs + configure_match_criteria(network_access, site, sdp, layer_type) + configure_slos(network_access, ietf_intent, layer_type) return site diff --git a/src/realizer/restconf/service_types/builders/initialize_structure.py b/src/realizer/restconf/service_types/builders/initialize_structure.py index 7d81f64..05c1267 100644 --- a/src/realizer/restconf/service_types/builders/initialize_structure.py +++ b/src/realizer/restconf/service_types/builders/initialize_structure.py @@ -29,10 +29,9 @@ def initialize_structure(vpn_id, connectivity_type, layer_type): } } if layer_type == "l2": - structure[f"ietf-{layer_type}vpn-svc:{layer_type}vpn-svc"]["vpn-services"]["vpn-service"] = { + structure[f"ietf-{layer_type}vpn-svc:{layer_type}vpn-svc"]["vpn-services"]["vpn-service"][0] = { "vpn-id": vpn_id, - "customer-name": "osm", - "vpn-svc-type": "vpws", # Lo dejamos en vpws porque es el unico que encaja - "svc-topo": connectivity_type, + "ce-vlan-preservation": False, + "ce-vlan-cos-preservation": False } return structure \ No newline at end of file -- GitLab From 54310374e9e6d90f6142afbfa7c1a13027df9c52 Mon Sep 17 00:00:00 2001 From: velazquez Date: Fri, 8 May 2026 12:02:34 +0000 Subject: [PATCH 07/27] Add match type "any" --- .../service_types/builders/configure_match_criteria.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index 5c0788e..6cd72c9 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -27,6 +27,7 @@ def configure_match_criteria(network_access, site, sdp, layer_type): MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too + MATCH_TYPE_MAPPING["any"] = "any" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: @@ -46,7 +47,7 @@ def configure_match_criteria(network_access, site, sdp, layer_type): prefix_length = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-prefix-length"]) lan = f"{provider_address}/{prefix_length}" if provider_address and prefix_length else None - if match_type != "vlan": + if match_type != "vlan" and match_type != "any": rule = { "id": f"match-{match_type}-{index}", "match-flow": { @@ -55,7 +56,8 @@ def configure_match_criteria(network_access, site, sdp, layer_type): } network_access["service"]["qos"]["qos-classification-policy"]["rule"].append(rule) - else: + elif match_type == "vlan": + site["routing-protocols"] = {"routing-protocol": []} # Handle VLAN match criteria routing_protocol = { "type": "static", -- GitLab From d7710b9b9f3fdbe12f33de693355d0a354afb22d Mon Sep 17 00:00:00 2001 From: velazquez Date: Fri, 8 May 2026 12:57:17 +0000 Subject: [PATCH 08/27] Add vlan match to l2 services --- .../builders/configure_match_criteria.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index 6cd72c9..af7c8e9 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -21,13 +21,13 @@ from src.utils.safe_get import safe_get def configure_match_criteria(network_access, site, sdp, layer_type): """Configura los criterios de coincidencia en el acceso a la red.""" MATCH_TYPE_MAPPING = { - "dscp": "dscp" + "dscp": "dscp", + "vlan": "dot1q", + "any": "any" } if layer_type == "l3": MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" - MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too - MATCH_TYPE_MAPPING["any"] = "any" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: @@ -47,23 +47,13 @@ def configure_match_criteria(network_access, site, sdp, layer_type): prefix_length = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-ipv4-prefix-length"]) lan = f"{provider_address}/{prefix_length}" if provider_address and prefix_length else None - if match_type != "vlan" and match_type != "any": - rule = { - "id": f"match-{match_type}-{index}", - "match-flow": { - MATCH_TYPE_MAPPING[match_type]: value - } - } - - network_access["service"]["qos"]["qos-classification-policy"]["rule"].append(rule) - elif match_type == "vlan": + if layer_type == "l3" and match_type == "vlan": site["routing-protocols"] = {"routing-protocol": []} - # Handle VLAN match criteria routing_protocol = { "type": "static", - "static":{ + "static": { "cascaded-lan-prefixes": { - "ipv4-lan-prefixes":[ + "ipv4-lan-prefixes": [ { "lan": lan, "lan-tag": value, @@ -75,3 +65,17 @@ def configure_match_criteria(network_access, site, sdp, layer_type): } site["routing-protocols"]["routing-protocol"].append(routing_protocol) + return + + # Do not add rule when match type is any + if match_type == "any": + return + + rule = { + "id": f"match-{match_type}-{index}", + "match-flow": { + MATCH_TYPE_MAPPING[match_type]: value + } + } + + network_access["service"]["qos"]["qos-classification-policy"]["rule"].append(rule) -- GitLab From c375c711f526dae071bbc7304c978c39aba92d31 Mon Sep 17 00:00:00 2001 From: velazquez Date: Fri, 8 May 2026 13:55:35 +0000 Subject: [PATCH 09/27] Fix bug --- src/api/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 1d4dacb..da437a7 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -659,7 +659,7 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["TFS_IP"], slice_type, slice.get("id")) + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -712,7 +712,7 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["TFS_IP"], slice_type, existing_slice.get("id")) + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, existing_slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -732,7 +732,7 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["TFS_IP"], slice_type, slice.get("id")) + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) -- GitLab From a5d6de44fd6a1b0367493ba740191a0da4095fcc Mon Sep 17 00:00:00 2001 From: velazquez Date: Tue, 19 May 2026 13:39:16 +0200 Subject: [PATCH 10/27] Service_db creation with all functions to access de database Modify slice creation and deletion to also change the service database --- app.py | 6 +- src/api/main.py | 19 ++- src/config/.env.example | 2 +- src/database/service_db.py | 233 +++++++++++++++++++++++++++++++++++++ src/mapper/main.py | 4 + 5 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 src/database/service_db.py diff --git a/app.py b/app.py index c87f2c3..2104af8 100644 --- a/app.py +++ b/app.py @@ -25,7 +25,8 @@ from swagger.restconf_namespace import restconf_ns from src.config.constants import NSC_PORT from src.webui.gui import gui_bp from src.config.config import create_config -from src.database.db import init_db +from src.database.db import init_db as init_slice +from src.database.service_db import init_db as init_service # Paths that do not require authentication (Swagger UI and its static assets) # /nsc → Swagger UI @@ -55,7 +56,8 @@ def _check_basic_auth(app): def create_app(): """Create Flask application with configured API and namespaces.""" - init_db() + init_slice() + init_service() app = Flask(__name__) app = create_config(app) app.logger.setLevel(logging.INFO) diff --git a/src/api/main.py b/src/api/main.py index da437a7..4d9799b 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -25,6 +25,7 @@ from src.utils.send_response import send_response import logging 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 from src.realizer.tfs.helpers.tfs_connector import tfs_connector 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 @@ -659,7 +660,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) + services = get_data_by_slice_id(slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -712,7 +717,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, existing_slice.get("id")) + services = get_data_by_slice_id(existing_slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -732,7 +741,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) + services = get_data_by_slice_id(slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) diff --git a/src/config/.env.example b/src/config/.env.example index 513c6db..3460187 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -66,7 +66,7 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=192.168.27.165 +RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO diff --git a/src/database/service_db.py b/src/database/service_db.py new file mode 100644 index 0000000..e897bab --- /dev/null +++ b/src/database/service_db.py @@ -0,0 +1,233 @@ +# Copyright 2022-2026 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 sqlite3, logging + +# Database file +DB_NAME = "service.db" + +# Initialize database and create table +def init_db(): + """ + Initialize the SQLite database and create the service table if not exists. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS service ( + service_id TEXT PRIMARY KEY, + slice_id TEXT NOT NULL + ) + """) + conn.commit() + conn.close() + +# Save data to the database +def save_data(service_id: str, slice_id: str): + """ + Save a new service entry to the database. + + Args: + service_id (str): Unique identifier for the service + slice_id (dict): Unique identifier for the slice + + Raises: + ValueError: If a service with the given service_id already exists + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO service (service_id, slice_id) VALUES (?, ?)", (service_id, slice_id)) + conn.commit() + # Handle duplicate service ID + except sqlite3.IntegrityError: + raise ValueError(f"Service with id '{service_id}' already exists.") + finally: + conn.close() + +# Update data in the database +def update_data(service_id: str, new_slice_id: str): + """ + Update an existing service entry in the database. + + Args: + service_id (str): Unique identifier for the service + slice_id (str): Unique identifier for the slice + + Raises: + ValueError: If no service is found with the given service_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("UPDATE service SET slice_id = ? WHERE service_id = ?", (new_slice_id, service_id)) + if cursor.rowcount == 0: + raise ValueError(f"No slice found with id '{service_id}' to update.") + else: + logging.debug(f"Slice '{service_id}' updated.") + conn.commit() + conn.close() + +# Delete data from the database +def delete_data(service_id: str): + """ + Delete a service entry from the database. + + Args: + service_id (str): Unique identifier for the service to delete + + Raises: + ValueError: If no service is found with the given service_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM service WHERE service_id = ?", (service_id,)) + if cursor.rowcount == 0: + raise ValueError(f"No service found with id '{service_id}' to delete.") + else: + logging.debug(f"Service '{service_id}' deleted.") + conn.commit() + conn.close() + +# Get data from the database +def get_data(service_id: str) -> dict[str, str]: + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM service WHERE service_id = ?", (service_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise ValueError(f"No service found with id '{service_id}'.") + + column_names = [desc[0] for desc in cursor.description] + conn.close() + + return dict(zip(column_names, row)) + + +# Get all services +def get_all_data() -> list[dict[str, str]]: + """ + Retrieve all service entries from the database. + + Returns: + list: List of service data dictionaries including service_id and slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("SELECT * FROM service") + + rows = cursor.fetchall() + column_names = [description[0] for description in cursor.description] + + conn.close() + + return [dict(zip(column_names, row)) for row in rows] + + +def delete_all_data(): + """ + Delete all service entries from the database. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM service") + conn.commit() + conn.close() + logging.debug("All service data deleted.") + + +def get_data_by_slice_id(slice_id: str) -> list[dict[str, str]]: + """ + Retrieve all service entries associated with a given slice_id. + + Args: + slice_id (str): Identifier of the slice + + Returns: + list: List of service data dictionaries including service_id and slice_id + + Raises: + ValueError: If no services are found with the given slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM service WHERE slice_id = ?", (slice_id,)) + rows = cursor.fetchall() + + if not rows: + conn.close() + raise ValueError(f"No services found with slice_id '{slice_id}'.") + + column_names = [desc[0] for desc in cursor.description] + conn.close() + + return [dict(zip(column_names, row)) for row in rows] + +def delete_by_slice_id(slice_id: str): + """ + Delete all service entries associated with a given slice_id. + + Args: + slice_id (str): Identifier of the slice whose services will be deleted + + Raises: + ValueError: If no services are found with the given slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("DELETE FROM service WHERE slice_id = ?", (slice_id,)) + + if cursor.rowcount == 0: + conn.close() + raise ValueError(f"No services found with slice_id '{slice_id}' to delete.") + + logging.debug(f"All services with slice_id '{slice_id}' deleted.") + + conn.commit() + conn.close() + + +# Example usage +if __name__ == "__main__": + init_db() + + # Save a service + save_data("service-001", "slice-001") + save_data("service-002", "slice-001") + save_data("service-003", "slice-002") + + # Get the service + result = get_data("service-001") + if result: + print(f"Retrieved service for service-001: {result}") + + # Update the service + update_data("service-001", "slice-002") + + # Delete the service + delete_data("service-001") + + delete_by_slice_id("slice-001") + + + result = get_all_data() + if result: + print(f"Retrieved data: {result}") + delete_all_data() diff --git a/src/mapper/main.py b/src/mapper/main.py index 6c6601d..6a3b160 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -23,6 +23,7 @@ from .process_connnectivity import process_connectivity from src.realizer.main import realizer from flask import current_app from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data +from src.database.service_db import save_data, update_data def mapper(ietf_intent, controller_type="TFS"): """ @@ -158,6 +159,9 @@ def mapper(ietf_intent, controller_type="TFS"): "way": way } services.append(service) + if not current_app.config["DUMMY_MODE"]: + # Save mapping from service_id to slice_id + save_data(service_id=safe_get(service, ["id"]), slice_id=service_id) logging.debug(f"Service added: {service}") # Break only for point-to-point -- GitLab From 8bb0fb8cd8ce72a60929506546cae7998bfc3adc Mon Sep 17 00:00:00 2001 From: velazquez Date: Wed, 20 May 2026 09:40:24 +0200 Subject: [PATCH 11/27] Update code commentaries --- src/api/main.py | 8 +- src/database/sysrepo_store.py | 84 +++++++++---------- src/mapper/extract_sdp_info.py | 4 +- src/mapper/slo_viability.py | 2 +- src/realizer/ixia/helpers/NEII_V4.py | 10 +-- src/realizer/ixia/main.py | 4 +- .../service_types/builders/configure_slos.py | 8 +- .../builders/create_site_from_sdp.py | 2 +- src/realizer/restconf/service_types/l2vpn.py | 2 +- src/realizer/restconf/service_types/l3vpn.py | 2 +- src/tests/test_e2e.py | 16 ++-- src/tests/test_initialization.py | 4 +- src/tests/test_nbi_processor.py | 12 +-- src/webui/gui.py | 6 +- 14 files changed, 82 insertions(+), 82 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 4d9799b..18367bf 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -460,7 +460,7 @@ class Api: if not existing_data: return send_response(False, code=404, message="Network slice services not found") - # Si no está en modo DUMMY, procesar con TFS + # If not in DUMMY mode, process with TFS result = self.slice_service.nsc(intent) if not result: return send_response(False, code=500, message="Failed to process slice in TFS") @@ -515,7 +515,7 @@ class Api: if not result: return send_response(False, code=500, message="Slice not updated") - # Eliminar el ID del body si existe (ya está en el predicado) + # Remove the ID from the body if it exists (it's already in the predicate) template_data = template.copy() template_data.pop("id", None) @@ -585,7 +585,7 @@ class Api: if not result: return send_response(False, code=500, message="Slice not updated") - # Eliminar el ID del body (ya está en el predicado) + # Remove the ID from the body (it's already in the predicate) intent_data = intent.copy() intent_data.pop("id", None) @@ -628,7 +628,7 @@ class Api: if "id" in sdp and sdp["id"] != sdp_id: return send_response(False, code=400, message="SDP ID in body does not match URL") - # Eliminar el ID del body (ya está en el predicado) + # Remove the ID from the body (it's already in the predicate) sdp_data = sdp.copy() sdp_data.pop("id", None) diff --git a/src/database/sysrepo_store.py b/src/database/sysrepo_store.py index 22b08a6..15cb874 100644 --- a/src/database/sysrepo_store.py +++ b/src/database/sysrepo_store.py @@ -21,13 +21,13 @@ import logging def _get_connection(): """ - Crea una nueva conexión cada vez + Creates a new connection each time """ return sysrepo.SysrepoConnection() def create_data_store(intent: dict, xpath: str= ""): """ - CREATE: Crea nuevos datos en el datastore + CREATE: Creates new data in the datastore """ conn = _get_connection() sess = conn.start_session() @@ -48,7 +48,7 @@ def create_data_store(intent: dict, xpath: str= ""): def get_data_store(xpath: str = ""): """ - Obtiene todos los slices del datastore + Gets all slices from the datastore """ conn = _get_connection() sess = conn.start_session() @@ -69,23 +69,23 @@ def get_data_store(xpath: str = ""): def update_data_store(intent: dict, xpath: str = ""): """ - UPDATE: Modifica datos en el datastore - + UPDATE: Modifies data in the datastore + Args: - intent: Datos a modificar - xpath: Ruta al recurso - operation: - - "merge" (default): PATCH - actualiza campos específicos - - "replace": PUT - reemplaza completamente el recurso - - "create": POST - solo crea si no existe - + intent: Data to modify + xpath: Path to resource + operation: + - "merge" (default): PATCH - updates specific fields + - "replace": PUT - completely replaces the resource + - "create": POST - only creates if it doesn't exist + Returns: - bool: True si tuvo éxito + bool: True if successful """ conn = _get_connection() sess = conn.start_session() - # PUT: Eliminar y recrear (reemplazo completo) + # PUT: Delete and recreate (complete replacement) try: if not xpath: sess.delete_item("/ietf-network-slice-service:network-slice-services") @@ -107,25 +107,25 @@ def update_data_store(intent: dict, xpath: str = ""): def patch_data_store(intent: dict, xpath: str = ""): """ - UPDATE: Modifica datos en el datastore - + UPDATE: Modifies data in the datastore + Args: - intent: Datos a modificar - xpath: Ruta al recurso - operation: - - "merge" (default): PATCH - actualiza campos específicos - - "replace": PUT - reemplaza completamente el recurso - - "create": POST - solo crea si no existe - + intent: Data to modify + xpath: Path to resource + operation: + - "merge" (default): PATCH - updates specific fields + - "replace": PUT - completely replaces the resource + - "create": POST - only creates if it doesn't exist + Returns: - bool: True si tuvo éxito + bool: True if successful """ conn = _get_connection() sess = conn.start_session() - - # PUT: Eliminar y recrear (reemplazo completo) + + # PUT: Delete and recreate (complete replacement) try: - # PATCH: Merge con datos existentes + # PATCH: Merge with existing data _write_dict(sess, xpath, intent) sess.apply_changes() logging.debug(f"Merged data at {xpath}") @@ -141,7 +141,7 @@ def patch_data_store(intent: dict, xpath: str = ""): def delete_data_store(xpath: str = ""): """ - DELETE: Elimina datos del datastore + DELETE: Deletes data from the datastore """ conn = _get_connection() sess = conn.start_session() @@ -161,8 +161,8 @@ def delete_data_store(xpath: str = ""): def _write_dict(sess, base_xpath, data, parent_key=None): """ - Convierte dict → XPaths YANG - parent_key: clave que ya está en el xpath y debe ser excluida + Converts dict → YANG XPaths + parent_key: key already in xpath and should be excluded """ LIST_KEYS = { 'slo-sle-template': 'id', @@ -201,13 +201,13 @@ def _write_dict(sess, base_xpath, data, parent_key=None): # ----------------------------- if isinstance(data, dict): - # Saltar diccionarios vacíos + # Skip empty dictionaries if not data: logging.debug("Skipping empty dict") return - + for k, v in data.items(): - # IMPORTANTE: Saltar la clave si ya está en el predicado + # IMPORTANT: Skip the key if already in the predicate if k == parent_key: logging.debug(f"Skipping key field '{k}' (already in predicate)") continue @@ -219,7 +219,7 @@ def _write_dict(sess, base_xpath, data, parent_key=None): # ----------------------------- elif isinstance(data, list): - # Saltar listas vacías + # Skip empty lists if not data: logging.debug("Skipping empty list") return @@ -233,7 +233,7 @@ def _write_dict(sess, base_xpath, data, parent_key=None): if is_leaf_list: # ----------------------------- - # LEAF-LIST: Lista de valores simples + # LEAF-LIST: List of simple values # ----------------------------- logging.debug(f"Processing as LEAF-LIST: {list_name}") @@ -243,13 +243,13 @@ def _write_dict(sess, base_xpath, data, parent_key=None): continue logging.debug(f"Adding leaf-list value: {value}") - # En sysrepo, los leaf-lists se agregan con el mismo xpath - # pero múltiples valores + # In sysrepo, leaf-lists are added with the same xpath + # but multiple values sess.set_item(base_xpath, str(value)) else: # ----------------------------- - # LIST: Lista de containers + # LIST: List of containers # ----------------------------- logging.debug(f"Processing as LIST: {list_name}") key_field = LIST_KEYS.get(list_name) @@ -260,7 +260,7 @@ def _write_dict(sess, base_xpath, data, parent_key=None): item_xpath = f"{base_xpath}[{key_field}='{key_value}']" logging.debug(f"Using key '{key_field}={key_value}' for list '{list_name}'") - # Pasar el key_field para que sea excluido al procesar el item + # Pass the key_field to be excluded when processing the item _write_dict(sess, item_xpath, item, parent_key=key_field) else: logging.error(f"ERROR: No key '{key_field}' found in item for list '{list_name}'") @@ -292,18 +292,18 @@ def normalize_libyang_data(data): KeyedList = None if KeyedList and isinstance(data, KeyedList): - # KeyedList -> lista normal + # KeyedList -> normal list return [normalize_libyang_data(item) for item in data] elif isinstance(data, dict): - # Dict -> procesar valores recursivamente + # Dict -> process values recursively return { key: normalize_libyang_data(value) for key, value in data.items() } elif isinstance(data, (list, tuple)): - # Lista/tupla -> procesar elementos recursivamente + # List/tuple -> process elements recursively return [normalize_libyang_data(item) for item in data] else: diff --git a/src/mapper/extract_sdp_info.py b/src/mapper/extract_sdp_info.py index dafbcf5..1c77831 100644 --- a/src/mapper/extract_sdp_info.py +++ b/src/mapper/extract_sdp_info.py @@ -32,12 +32,12 @@ def extract_sdp_info(sdp_id, slice_service, connection_group_id, connectivity_co logging.debug(f"Looking for match criteria with target-connectivity-construct-id: {connectivity_construct_id}") selected_match_criteria = next((mc for mc in match_criteria_list if safe_get(mc, ["target-connectivity-construct-id"]) == connectivity_construct_id), None) - # Si no, buscar por connection group + # If not, search by connection group if not selected_match_criteria and connection_group_id: logging.debug(f"Looking for match criteria with target-connection-group-id: {connection_group_id}") selected_match_criteria = next((mc for mc in match_criteria_list if safe_get(mc, ["target-connection-group-id"]) == connection_group_id), None) - # Si no, usar el primero disponible + # If not, use the first available if not selected_match_criteria: logging.debug("No specific match criteria found for connectivity construct or connection group. Using the first available match criteria.") selected_match_criteria = match_criteria_list[0] diff --git a/src/mapper/slo_viability.py b/src/mapper/slo_viability.py index 3634a17..cc6ea62 100644 --- a/src/mapper/slo_viability.py +++ b/src/mapper/slo_viability.py @@ -61,4 +61,4 @@ def slo_viability(slice_slos, nrp_slos): # Calculate final viability score score = sum(flexibility_scores) / len(flexibility_scores) if flexibility_scores else 0 - return True, score # Si pasó todas las verificaciones, la NRP es viable \ No newline at end of file + return True, score # If it passed all verifications, the NRP is viable \ No newline at end of file diff --git a/src/realizer/ixia/helpers/NEII_V4.py b/src/realizer/ixia/helpers/NEII_V4.py index 4f9a249..16ddaeb 100644 --- a/src/realizer/ixia/helpers/NEII_V4.py +++ b/src/realizer/ixia/helpers/NEII_V4.py @@ -41,7 +41,7 @@ class NEII_controller: self.existentes(ip) return - ## FUNCIONES MENÚ PRINCIPAL ## + ## MAIN MENU FUNCTIONS ## def ver_info(self,ip): ''' @@ -173,9 +173,9 @@ class NEII_controller: drops = json_data.get("drops", None) desv_drop = json_data.get("desv_drop", None) - # --- Variables de configuración --- + # --- Configuration variables --- - # Configuración de IPv4 / IPv6 + # IPv4 / IPv6 configuration if src_node_ip and dst_node_ip: if isinstance(ipaddress.ip_address(src_node_ip), ipaddress.IPv4Address) and isinstance(ipaddress.ip_address(dst_node_ip), ipaddress.IPv4Address): configuraciones['ipv4'] = self.ipv4(src_node_ip, dst_node_ip, 5) @@ -214,12 +214,12 @@ class NEII_controller: dataProfile['profiles'].append(configuracion_perfil) logging.info(f"Configuración del perfil: {configuracion_perfil}") - # Enviar la configuración + # Send the configuration automatizacion.envio_peticion(ip, puerto, dataProfile) return automatizacion.obtener_informacion_puerto(ip, puerto) - ## FUNCIONES DE CONFIGURACIÓN DE PUERTO ## + ## PORT CONFIGURATION FUNCTIONS ## def delay(self,delay_perfil): ''' diff --git a/src/realizer/ixia/main.py b/src/realizer/ixia/main.py index 5bcfcce..ff3c890 100644 --- a/src/realizer/ixia/main.py +++ b/src/realizer/ixia/main.py @@ -44,7 +44,7 @@ def ixia(ietf_intent): latency = None tolerance = None - # Asignar valores según el tipo de métrica + # Assign values according to metric type for metric in metric_bounds: metric_type = metric.get("metric-type") bound = metric.get("bound") @@ -56,7 +56,7 @@ def ixia(ietf_intent): elif metric_type == "one-way-delay-variation-maximum": tolerance = bound - # Construcción del diccionario intent + # Construction of the intent dictionary intent = { "src_node_ip": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) .get("slice-service", [{}])[0] diff --git a/src/realizer/restconf/service_types/builders/configure_slos.py b/src/realizer/restconf/service_types/builders/configure_slos.py index 6a4f5d1..7721e51 100644 --- a/src/realizer/restconf/service_types/builders/configure_slos.py +++ b/src/realizer/restconf/service_types/builders/configure_slos.py @@ -25,23 +25,23 @@ def configure_slos(network_access, ietf_intent, layer_type): logging.debug(f"Configuring SLOs with constraints: {safe_get(ietf_intent, ['template', 'slo-policy', 'metric-bound'])}") - # Configurar constraints de métricas + # Configure metric constraints metric_bounds = safe_get(ietf_intent, ["template", "slo-policy", "metric-bound"]) if metric_bounds: for constraint in metric_bounds: apply_metric_constraint(service, qos_class, constraint, ietf_intent["id"], layer_type) - # Configurar availability + # Configure availability availability = safe_get(ietf_intent, ["template", "slo-policy", "availability"]) if availability: qos_class.setdefault("bandwidth", {})["guaranteed-bw-percent"] = availability - # Configurar MTU + # Configure MTU mtu = safe_get(ietf_intent, ["template", "slo-policy", "mtu"]) if mtu: service["svc-mtu"] = mtu - # Configurar availability y MTU por defecto si no se han configurado + # Configure availability and MTU defaults if not configured if "guaranteed-bw-percent" not in qos_class.get("bandwidth", {}): qos_class.setdefault("bandwidth", {})["guaranteed-bw-percent"] = 0 if "svc-mtu" not in service: diff --git a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py index 5f05f42..124fbb2 100644 --- a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py +++ b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py @@ -34,7 +34,7 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): """ logging.debug(f"Processing SDP: {sdp}") - # Extraer información básica + # Extract basic information location = safe_get(sdp, ["sdp", "node-id"]) router_id = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-node-id"]) router_if = safe_get(sdp, ["sdp", "attachment-circuits", "attachment-circuit", 0, "ac-tp-id"]) diff --git a/src/realizer/restconf/service_types/l2vpn.py b/src/realizer/restconf/service_types/l2vpn.py index 7f17235..465e86e 100644 --- a/src/realizer/restconf/service_types/l2vpn.py +++ b/src/realizer/restconf/service_types/l2vpn.py @@ -31,7 +31,7 @@ def l2vpn(ietf_intent): Returns: Diccionario con la configuración del servicio L2VPN o None si no hay SDPs """ - # Validación temprana + # Early validation if not ietf_intent.get("sdps"): logging.warning("SDPs not found in the intent. Skipping L2VPN realization.") return None diff --git a/src/realizer/restconf/service_types/l3vpn.py b/src/realizer/restconf/service_types/l3vpn.py index 1e73866..78a8493 100644 --- a/src/realizer/restconf/service_types/l3vpn.py +++ b/src/realizer/restconf/service_types/l3vpn.py @@ -29,7 +29,7 @@ def l3vpn(ietf_intent): Returns: Diccionario con la configuración del servicio L3VPN o None si no hay SDPs """ - # Validación temprana + # Early validation if not ietf_intent.get("sdps"): logging.warning("SDPs not found in the intent. Skipping L3VPN realization.") return None diff --git a/src/tests/test_e2e.py b/src/tests/test_e2e.py index 6e9f2e4..36ed12f 100644 --- a/src/tests/test_e2e.py +++ b/src/tests/test_e2e.py @@ -22,10 +22,10 @@ from src.api.main import Api from src.main import NSController from app import create_app -# Carpeta donde están los JSON de requests +# Folder where request JSONs are located REQUESTS_DIR = Path(__file__).parent / "requests" -# Lista de todos los flags booleanos que quieres probar +# List of all boolean flags you want to test FLAGS_TO_TEST = ["WEBUI_DEPLOY", "DUMP_TEMPLATES", "PLANNER_ENABLED", "PCE_EXTERNAL", "NRP_ENABLED"] # Valores posibles para PLANNER_TYPE @@ -67,7 +67,7 @@ def temp_sqlite_db(monkeypatch, tmp_path): if temp_db_path.exists(): temp_db_path.unlink() -# Función para cargar todos los JSONs +# Function to load all JSONs def load_request_files(): test_cases = [] for f in REQUESTS_DIR.glob("*.json"): @@ -76,7 +76,7 @@ def load_request_files(): test_cases.append(json_data) return test_cases -# Generador de todas las combinaciones de flags +# Generator of all flag combinations def generate_flag_combinations(): bool_values = [True, False] for combo in product(bool_values, repeat=len(FLAGS_TO_TEST)): @@ -85,7 +85,7 @@ def generate_flag_combinations(): yield {**bool_flags, "PLANNER_TYPE": planner_type} -# Fixture que combina cada request con cada combinación de flags +# Fixture that combines each request with each combination of flags def generate_test_cases(): requests = load_request_files() for json_data in requests: @@ -104,14 +104,14 @@ def test_add_and_delete_flow(app, json_data, flags, expected_codes, set_flags, t controller = NSController(controller_type="TFS") api = Api(controller) - # Añadir flujo + # Add flow data, code = api.add_flow(json_data) assert code in expected_codes, f"Flags en fallo: {flags}" - # Eliminar flujo si fue creado + # Delete flow if it was created if code == 201 and isinstance(data, dict) and "slice_id" in data: slice_id = data["slice_id"] _, delete_code = api.delete_flows(slice_id=slice_id) - assert delete_code == 204, f"No se pudo eliminar el slice {slice_id}" + assert delete_code == 204, f"Could not delete slice {slice_id}" diff --git a/src/tests/test_initialization.py b/src/tests/test_initialization.py index 3500465..629d590 100644 --- a/src/tests/test_initialization.py +++ b/src/tests/test_initialization.py @@ -16,7 +16,7 @@ import pytest -# Importa tu clase (ajusta el nombre del módulo si es distinto) +# Import your class (adjust the module name if different) from src.main import NSController def test_init_default_values(): @@ -47,7 +47,7 @@ def test_init_independence_between_instances(): # Modifico una lista en una instancia c1.response.append("test-response") - # La otra instancia no debería verse afectada + # The other instance should not be affected assert c2.response == [] assert c1.response == ["test-response"] diff --git a/src/tests/test_nbi_processor.py b/src/tests/test_nbi_processor.py index 40db65e..fecb109 100644 --- a/src/tests/test_nbi_processor.py +++ b/src/tests/test_nbi_processor.py @@ -45,7 +45,7 @@ def ietf_intent(): @pytest.fixture def gpp_intent(): - # Estructura mínima consistente con translator + # Minimum structure consistent with translator return { "RANSliceSubnet1": { "networkSliceSubnetRef": ["subnetA", "subnetB"] @@ -95,7 +95,7 @@ def gpp_intent(): @pytest.fixture def fake_template(): - # Plantilla mínima para que el traductor funcione + # Minimum template for translator to work return { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { @@ -176,7 +176,7 @@ def test_detect_format_invalid_types(data): def test_detect_format_multiple_keys(): - # Si tiene IETF y 3GPP, debe priorizar IETF + # If it has IETF and 3GPP, should prioritize IETF data = { "ietf-network-slice-service:network-slice-services": {}, "RANSliceSubnet1": {} @@ -187,7 +187,7 @@ def test_detect_format_multiple_keys(): # ---------- Extra nbi_processor ---------- def test_nbi_processor_gpp_missing_refs(gpp_intent): - # Quitar networkSliceSubnetRef debería provocar ValueError en translator loop + # Removing networkSliceSubnetRef should cause ValueError in translator loop broken = gpp_intent.copy() broken["RANSliceSubnet1"] = {} # no tiene "networkSliceSubnetRef" with pytest.raises(KeyError): @@ -210,10 +210,10 @@ def test_translator_maps_metrics(mock_load_template, gpp_intent, fake_template): @patch("src.nbi_processor.translator.load_template") def test_translator_empty_profile(mock_load_template, gpp_intent, fake_template): mock_load_template.return_value = fake_template - gpp_intent["subnetA"]["SliceProfileList"] = [{}] # vacío + gpp_intent["subnetA"]["SliceProfileList"] = [{}] # empty result = translator(gpp_intent, "subnetA") metrics = result["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] - assert metrics == [] # no debería añadir nada + assert metrics == [] # should not add anything @patch("src.nbi_processor.translator.load_template") def test_translator_sdps_are_populated(mock_load_template, gpp_intent, fake_template): diff --git a/src/webui/gui.py b/src/webui/gui.py index 73db9f1..933b8cf 100644 --- a/src/webui/gui.py +++ b/src/webui/gui.py @@ -181,7 +181,7 @@ def __datos_json(): @gui_bp.route('/webui') def home(): session['enter'] = False - # Leer las IPs actuales del archivo de configuración + # Read the current IPs from the configuration file try: tfs_ip = current_app.config["TFS_IP"] ixia_ip = current_app.config["IXIA_IP"] @@ -204,7 +204,7 @@ def generate_tfs(): slice_request = __build_request_ietf(src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, bandwidth=bandwidth) - # Si 'request' es un diccionario, conviértelo a JSON + # If 'request' is a dictionary, convert it to JSON json_data = json.dumps(slice_request) files = { 'file': ('ietf_template_example.json', json_data, 'application/json') @@ -248,7 +248,7 @@ def generate_ixia(): if int(reliability)==100: reliability=None slice_request = __build_request_ietf(src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, bandwidth=bandwidth, latency=latency, latency_version=latency_version, tolerance=tolerance, reliability=reliability) - # Si 'request' es un diccionario, conviértelo a JSON + # If 'request' is a dictionary, convert it to JSON json_data = json.dumps(slice_request) files = { 'file': ('ietf_template_example.json', json_data, 'application/json') -- GitLab From 45ddd31f630457395a03c45bcf3e5b816016e064 Mon Sep 17 00:00:00 2001 From: velazquez Date: Wed, 20 May 2026 10:08:23 +0200 Subject: [PATCH 12/27] More comments to english --- src/api/main.py | 34 ++++++------- src/mapper/process_connnectivity.py | 1 - .../builders/create_site_from_sdp.py | 14 +++--- src/realizer/restconf/service_types/l2vpn.py | 16 +++--- src/realizer/restconf/service_types/l3vpn.py | 16 +++--- src/tests/test_api.py | 46 +++++++++-------- src/tests/test_e2e.py | 30 ++++++------ src/tests/test_utils.py | 49 +++++++++---------- 8 files changed, 100 insertions(+), 106 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 18367bf..ed1e165 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -450,12 +450,12 @@ class Api: def update_network_slice_service(self, intent): """ - Modifica (reemplaza) toda la configuración de network-slice-services + Modify (replace) all network-slice-services configuration """ try: xpath = "/ietf-network-slice-service:network-slice-services" - # Verificar que existe algo que modificar + # Verify if there is something to modify existing_data = get_data_store(xpath) if not existing_data: return send_response(False, code=404, message="Network slice services not found") @@ -465,7 +465,7 @@ class Api: if not result: return send_response(False, code=500, message="Failed to process slice in TFS") - # Reemplazar completamente el recurso + # Replace completely the resource update_data_store(intent) logging.info("Network slice services modified successfully") @@ -483,17 +483,17 @@ class Api: def update_slo_sle_template(self, template_id, template): """ - Modifica (reemplaza) un template SLO/SLE específico + Modify (replace) an specific SLO/SLE template """ try: xpath = f"/ietf-network-slice-service:network-slice-services/slo-sle-templates/slo-sle-template[id='{template_id}']" - # Verificar que el template existe + # Verify the template exists existing_template = get_data_store(xpath) if not existing_template: return send_response(False, code=404, message="Template not found") - # Asegurar que el ID en el body coincide con el de la URL + # Assure that the body ID matches the URL if "id" in template and template["id"] != template_id: return send_response(False, code=400, message="Template ID in body does not match URL") @@ -519,7 +519,7 @@ class Api: template_data = template.copy() template_data.pop("id", None) - # Reemplazar el template + # Replace the template update_data_store(template_data, xpath) logging.info(f"Template {template_id} modified successfully") @@ -542,16 +542,16 @@ class Api: try: xpath = f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']" - # Verificar que el slice existe + # Verify that the slice exists existing_slice = get_data_store(xpath) if not existing_slice: return send_response(False, code=404, message="Slice not found") - # Asegurar que el ID en el body coincide con el de la URL + # Assure that the body ID matches the URL if "id" in intent and intent["id"] != slice_id: return send_response(False, code=400, message="Slice ID in body does not match URL") - # Validar que existe el template SLO/SLE referenciado + # Validate that the referenced SLO/SLE template exists if "slo-sle-template" in intent: template_ref = intent.get("slo-sle-template") xpath_template = f"/ietf-network-slice-service:network-slice-services/slo-sle-templates/slo-sle-template[id='{template_ref}']" @@ -559,7 +559,7 @@ class Api: if not existing_template: return send_response(False, code=404, message="Referenced SLO/SLE template not found") - # Construir el intent completo para TFS + # Build the full intent full_intent = { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { @@ -589,7 +589,7 @@ class Api: intent_data = intent.copy() intent_data.pop("id", None) - # Reemplazar el slice + # Replace the slice update_data_store(intent_data, xpath) logging.info(f"Slice {slice_id} modified successfully") @@ -609,22 +609,22 @@ class Api: def update_sdp(self, slice_id, sdp_id, sdp): """ - Modifica (reemplaza) un SDP específico dentro de un slice + Modify (replace) an specific SDP in the slice """ try: - # Verificar que el slice existe + # Verify the template exists slice_xpath = f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']" existing_slice = get_data_store(slice_xpath) if not existing_slice: return send_response(False, code=404, message="Slice not found") - # Verificar que el SDP existe + # Verify the SDP exists sdp_xpath = f"/ietf-network-slice-service:network-slice-services/slice-service[id='{slice_id}']/sdps/sdp[id='{sdp_id}']" existing_sdp = get_data_store(sdp_xpath) if not existing_sdp: return send_response(False, code=404, message="SDP not found") - # Asegurar que el ID en el body coincide con el de la URL + # Assure that the body ID matches the URL if "id" in sdp and sdp["id"] != sdp_id: return send_response(False, code=400, message="SDP ID in body does not match URL") @@ -632,7 +632,7 @@ class Api: sdp_data = sdp.copy() sdp_data.pop("id", None) - # Reemplazar el SDP + # Replace the SDP update_data_store(sdp_data, sdp_xpath) logging.info(f"SDP {sdp_id} in slice {slice_id} modified successfully") diff --git a/src/mapper/process_connnectivity.py b/src/mapper/process_connnectivity.py index 8653124..b057ce1 100644 --- a/src/mapper/process_connnectivity.py +++ b/src/mapper/process_connnectivity.py @@ -16,7 +16,6 @@ from src.utils.safe_get import safe_get from .extract_sdp_info import extract_sdp_info -import logging def process_connectivity(connection_group_id, connectivity_type, connectivity_construct, connectivity_construct_id, slice_service): """ diff --git a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py index 124fbb2..b854364 100644 --- a/src/realizer/restconf/service_types/builders/create_site_from_sdp.py +++ b/src/realizer/restconf/service_types/builders/create_site_from_sdp.py @@ -22,15 +22,15 @@ from .configure_slos import configure_slos def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): """ - Crea la configuración de un site a partir de un SDP. - + Creates the configuration of a site from an SDP. + Args: sdp: Service Delivery Point - ietf_intent: Intent IETF completo - connectivity_type: Tipo de conectividad - + ietf_intent: Complete IETF intent + connectivity_type: Connectivity type + Returns: - Diccionario con la configuración del site + Dictionary with site configuration """ logging.debug(f"Processing SDP: {sdp}") @@ -43,7 +43,7 @@ def create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type): network_access = create_network_access(sdp, ietf_intent, connectivity_type, router_id, router_if, layer_type) - # Crear estructura del site + # Create site structure site = { "site-id": safe_get(sdp, ["sdp", "id"]), "locations": { diff --git a/src/realizer/restconf/service_types/l2vpn.py b/src/realizer/restconf/service_types/l2vpn.py index 465e86e..5d48474 100644 --- a/src/realizer/restconf/service_types/l2vpn.py +++ b/src/realizer/restconf/service_types/l2vpn.py @@ -22,26 +22,26 @@ from .builders.create_site_from_sdp import create_site_from_sdp def l2vpn(ietf_intent): """ - Crea un servicio L2VPN basado en el intent IETF proporcionado. - + Creates an L2VPN service based on the provided IETF intent. + Args: - ietf_intent: Diccionario con la configuración del intent IETF - response: Objeto de respuesta - + ietf_intent: Dictionary with IETF intent configuration + response: Response object + Returns: - Diccionario con la configuración del servicio L2VPN o None si no hay SDPs + Dictionary with L2VPN service configuration or None if no SDPs """ # Early validation if not ietf_intent.get("sdps"): logging.warning("SDPs not found in the intent. Skipping L2VPN realization.") return None - # Inicializar estructura L2VPN + # Initialize L2VPN structure connectivity_type = ietf_intent["connectivity_type"] l2_service = initialize_structure(ietf_intent["id"], connectivity_type, layer_type="l2") - # Procesar cada SDP + # Process each SDP for sdp in ietf_intent["sdps"]: site = create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type="l2") l2_service["ietf-l2vpn-svc:l2vpn-svc"]["sites"]["site"].append(site) diff --git a/src/realizer/restconf/service_types/l3vpn.py b/src/realizer/restconf/service_types/l3vpn.py index 78a8493..2fdf7bb 100644 --- a/src/realizer/restconf/service_types/l3vpn.py +++ b/src/realizer/restconf/service_types/l3vpn.py @@ -20,25 +20,25 @@ from .builders.create_site_from_sdp import create_site_from_sdp def l3vpn(ietf_intent): """ - Crea un servicio L3VPN basado en el intent IETF proporcionado. - + Creates an L3VPN service based on the provided IETF intent. + Args: - ietf_intent: Diccionario con la configuración del intent IETF - response: Objeto de respuesta - + ietf_intent: Dictionary with IETF intent configuration + response: Response object + Returns: - Diccionario con la configuración del servicio L3VPN o None si no hay SDPs + Dictionary with L3VPN service configuration or None if no SDPs """ # Early validation if not ietf_intent.get("sdps"): logging.warning("SDPs not found in the intent. Skipping L3VPN realization.") return None - # Inicializar estructura L3VPN + # Initialize L3VPN structure connectivity_type = ietf_intent["connectivity_type"] l3_service = initialize_structure(ietf_intent["id"], connectivity_type, layer_type="l3") - # Procesar cada SDP + # Process each SDP for sdp in ietf_intent["sdps"]: site = create_site_from_sdp(sdp, ietf_intent, connectivity_type, layer_type="l3") l3_service["ietf-l3vpn-svc:l3vpn-svc"]["sites"]["site"].append(site) diff --git a/src/tests/test_api.py b/src/tests/test_api.py index 353198f..61d5151 100644 --- a/src/tests/test_api.py +++ b/src/tests/test_api.py @@ -32,7 +32,7 @@ load_dotenv() @pytest.fixture(scope="session") def flask_app(): - """Crea una app Flask mínima para los tests.""" + """Creates a minimal Flask app for tests.""" app = Flask(__name__) app.config.update({ "TESTING": True, @@ -54,13 +54,13 @@ def flask_app(): @pytest.fixture(autouse=True) def push_flask_context(flask_app): - """Empuja automáticamente un contexto Flask para cada test.""" + """Automatically pushes a Flask context for each test.""" with flask_app.app_context(): yield @pytest.fixture def temp_db(tmp_path): - """Fixture to create and cleanup test database using SQLite instead of JSON.""" + """Fixture to create and cleanup a test database using SQLite instead of JSON.""" test_db_name = str(tmp_path / "test_slice.db") # Create database with proper schema @@ -111,14 +111,14 @@ def env_variables(): @pytest.fixture def controller_with_mocked_db(temp_db): - """Crea un NSController con base de datos mockeada.""" + """Creates an NSController with a mocked database.""" with patch('src.database.db.DB_NAME', temp_db): yield NSController(controller_type="TFS") @pytest.fixture def ietf_intent(): - """Intent válido en formato IETF.""" + """Valid intent in IETF format.""" return { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { @@ -150,8 +150,8 @@ def ietf_intent(): { "match-type": [ { - "type": "vlan", - "vlan": [100] + "type": "vlan", + "vlan": [100] } ] } @@ -175,8 +175,8 @@ def ietf_intent(): { "match-type": [ { - "type": "vlan", - "vlan": [100] + "type": "vlan", + "vlan": [100] } ] } @@ -205,14 +205,14 @@ class TestBasicApiOperations: """Tests for basic API operations.""" def test_get_flows_empty(self, controller_with_mocked_db): - """Debe devolver error cuando no hay slices.""" + """Should return an error when there are no slices.""" result, code = Api(controller_with_mocked_db).get_flows() assert code == 404 assert result["success"] is False assert result["data"] is None def test_add_flow_success(self, controller_with_mocked_db, ietf_intent): - """Debe poder añadir un flow exitosamente.""" + """Should successfully add a flow.""" with patch('src.database.db.save_data') as mock_save: result, code = Api(controller_with_mocked_db).add_flow(ietf_intent) assert code == 201 @@ -220,7 +220,7 @@ class TestBasicApiOperations: assert "slices" in result["data"] def test_add_and_get_flow(self, controller_with_mocked_db, ietf_intent): - """Debe poder añadir un flow y luego recuperarlo.""" + """Should add a flow and then retrieve it.""" with patch('src.database.db.save_data') as mock_save, \ patch('src.database.db.get_all_data') as mock_get_all: @@ -239,7 +239,7 @@ class TestBasicApiOperations: assert any(s["slice_id"] == "slice-test-1" for s in flows) def test_modify_flow_success(self, controller_with_mocked_db, ietf_intent): - """Debe poder modificar un flow existente.""" + """Should successfully modify an existing flow.""" with patch('src.database.db.update_data') as mock_update: Api(controller_with_mocked_db).add_flow(ietf_intent) new_intent = ietf_intent.copy() @@ -251,7 +251,7 @@ class TestBasicApiOperations: assert result["success"] is True def test_delete_specific_flow_success(self, controller_with_mocked_db, ietf_intent): - """Debe borrar un flow concreto.""" + """Should delete a specific flow.""" with patch('src.database.db.delete_data') as mock_delete: Api(controller_with_mocked_db).add_flow(ietf_intent) result, code = Api(controller_with_mocked_db).delete_flows("slice-test-1") @@ -259,14 +259,14 @@ class TestBasicApiOperations: assert result == {} def test_delete_all_flows_success(self, controller_with_mocked_db): - """Debe borrar todos los flows.""" + """Should delete all flows.""" with patch('src.database.db.delete_all_data') as mock_delete_all: result, code = Api(controller_with_mocked_db).delete_flows() assert code == 204 assert result == {} def test_get_specific_flow(self, controller_with_mocked_db, ietf_intent): - """Debe poder recuperar un flow específico.""" + """Should retrieve a specific flow.""" with patch('src.database.db.get_data') as mock_get: Api(controller_with_mocked_db).add_flow(ietf_intent) mock_get.return_value = { @@ -284,19 +284,19 @@ class TestErrorHandling: """Tests for error handling.""" def test_add_flow_with_empty_intent(self, controller_with_mocked_db): - """Debe fallar si se pasa un intent vacío.""" + """Should fail if an empty intent is provided.""" result, code = Api(controller_with_mocked_db).add_flow({}) assert code in (400, 404, 500) assert result["success"] is False def test_add_flow_with_none(self, controller_with_mocked_db): - """Debe fallar si se pasa None como intent.""" + """Should fail if None is provided as intent.""" result, code = Api(controller_with_mocked_db).add_flow(None) assert code in (400, 500) assert result["success"] is False def test_get_nonexistent_slice(self, controller_with_mocked_db): - """Debe devolver 404 si se pide un slice inexistente.""" + """Should return 404 if a nonexistent slice is requested.""" with patch('src.database.db.get_data') as mock_get: mock_get.side_effect = ValueError("No slice found") @@ -305,7 +305,7 @@ class TestErrorHandling: assert result["success"] is False def test_modify_nonexistent_flow(self, controller_with_mocked_db, ietf_intent): - """Debe fallar si se intenta modificar un flow inexistente.""" + """Should fail if attempting to modify a nonexistent flow.""" with patch('src.database.db.update_data') as mock_update: mock_update.side_effect = ValueError("No slice found") @@ -314,12 +314,10 @@ class TestErrorHandling: assert result["success"] is False def test_delete_nonexistent_flow(self, controller_with_mocked_db): - """Debe fallar si se intenta eliminar un flow inexistente.""" + """Should fail if attempting to delete a nonexistent flow.""" with patch('src.database.db.delete_data') as mock_delete: mock_delete.side_effect = ValueError("No slice found") result, code = Api(controller_with_mocked_db).delete_flows("nonexistent") assert code == 404 - assert result["success"] is False - - + assert result["success"] is False \ No newline at end of file diff --git a/src/tests/test_e2e.py b/src/tests/test_e2e.py index 36ed12f..ce2e3e8 100644 --- a/src/tests/test_e2e.py +++ b/src/tests/test_e2e.py @@ -22,30 +22,30 @@ from src.api.main import Api from src.main import NSController from app import create_app -# Folder where request JSONs are located +# Folder where request JSON files are located REQUESTS_DIR = Path(__file__).parent / "requests" -# List of all boolean flags you want to test +# List of all boolean flags to test FLAGS_TO_TEST = ["WEBUI_DEPLOY", "DUMP_TEMPLATES", "PLANNER_ENABLED", "PCE_EXTERNAL", "NRP_ENABLED"] -# Valores posibles para PLANNER_TYPE +# Possible values for PLANNER_TYPE PLANNER_TYPE_VALUES = ["ENERGY", "HRAT", "TFS_OPTICAL"] @pytest.fixture def app(temp_sqlite_db): - """Crea la app Flask con configuración por defecto.""" + """Creates the Flask app with default configuration.""" app = create_app() return app @pytest.fixture def client(app): - """Cliente de test de Flask para hacer requests.""" + """Flask test client for making requests.""" return app.test_client() @pytest.fixture def set_flags(app): - """Cambia directamente los flags en app.config""" + """Directly updates flags in app.config.""" def _set(flags: dict): for k, v in flags.items(): app.config[k] = v @@ -53,21 +53,21 @@ def set_flags(app): @pytest.fixture def temp_sqlite_db(monkeypatch, tmp_path): - """Usa una base de datos SQLite temporal durante los tests.""" + """Uses a temporary SQLite database during tests.""" temp_db_path = tmp_path / "test_slice.db" monkeypatch.setattr("src.database.db.DB_NAME", str(temp_db_path)) - # Inicializa la base de datos temporal + # Initialize temporary database from src.database.db import init_db init_db() yield temp_db_path - # Limpieza al finalizar + # Cleanup after finishing if temp_db_path.exists(): temp_db_path.unlink() -# Function to load all JSONs +# Function to load all JSON files def load_request_files(): test_cases = [] for f in REQUESTS_DIR.glob("*.json"): @@ -76,7 +76,7 @@ def load_request_files(): test_cases.append(json_data) return test_cases -# Generator of all flag combinations +# Generator for all flag combinations def generate_flag_combinations(): bool_values = [True, False] for combo in product(bool_values, repeat=len(FLAGS_TO_TEST)): @@ -85,7 +85,7 @@ def generate_flag_combinations(): yield {**bool_flags, "PLANNER_TYPE": planner_type} -# Fixture that combines each request with each combination of flags +# Fixture combining each request with each flag combination def generate_test_cases(): requests = load_request_files() for json_data in requests: @@ -106,12 +106,10 @@ def test_add_and_delete_flow(app, json_data, flags, expected_codes, set_flags, t # Add flow data, code = api.add_flow(json_data) - assert code in expected_codes, f"Flags en fallo: {flags}" + assert code in expected_codes, f"Failed flags: {flags}" # Delete flow if it was created if code == 201 and isinstance(data, dict) and "slice_id" in data: slice_id = data["slice_id"] _, delete_code = api.delete_flows(slice_id=slice_id) - assert delete_code == 204, f"Could not delete slice {slice_id}" - - + assert delete_code == 204, f"Could not delete slice {slice_id}" \ No newline at end of file diff --git a/src/tests/test_utils.py b/src/tests/test_utils.py index 64d26bf..c8da1b3 100644 --- a/src/tests/test_utils.py +++ b/src/tests/test_utils.py @@ -16,8 +16,6 @@ import json import pytest -import os - from src.utils.load_template import load_template from src.utils.dump_templates import dump_templates from src.utils.send_response import send_response @@ -26,7 +24,7 @@ from flask import Flask @pytest.fixture def tmp_json_file(tmp_path): - """Crea un archivo JSON temporal válido y devuelve su ruta y contenido.""" + """Creates a valid temporary JSON file and returns its path and content.""" data = {"name": "test"} file_path = tmp_path / "template.json" file_path.write_text(json.dumps(data)) @@ -34,14 +32,14 @@ def tmp_json_file(tmp_path): def test_load_template_ok(tmp_json_file): - """Debe cargar correctamente un JSON válido.""" + """Should correctly load a valid JSON file.""" file_path, expected = tmp_json_file result = load_template(str(file_path)) assert result == expected def test_load_template_invalid(tmp_path): - """Debe devolver un response con error si el JSON es inválido.""" + """Should return an error response if the JSON is invalid.""" bad_file = tmp_path / "bad.json" bad_file.write_text("{invalid json}") @@ -51,7 +49,7 @@ def test_load_template_invalid(tmp_path): assert "Template loading error" in result["error"] def test_dump_templates_enabled(monkeypatch, tmp_path): - """Debe volcar múltiples JSON correctamente en src/templates si DUMP_TEMPLATES está activado.""" + """Should correctly dump multiple JSON files into src/templates when DUMP_TEMPLATES is enabled.""" templates_dir = tmp_path / "src" / "templates" templates_dir.mkdir(parents=True) @@ -73,7 +71,7 @@ def test_dump_templates_enabled(monkeypatch, tmp_path): assert json.loads(file_path.read_text()) == data def test_dump_templates_disabled(monkeypatch, tmp_path): - """No debe escribir nada en src/templates si DUMP_TEMPLATES está desactivado.""" + """Should not write anything into src/templates when DUMP_TEMPLATES is disabled.""" templates_dir = tmp_path / "src" / "templates" templates_dir.mkdir(parents=True) @@ -89,7 +87,7 @@ def test_dump_templates_disabled(monkeypatch, tmp_path): assert not (templates_dir / name).exists() def test_send_response_success(): - """Debe devolver success=True y code=200 si el resultado es True.""" + """Should return success=True and code=200 if the result is True.""" resp, code = send_response(True, data={"k": "v"}) assert code == 200 assert resp["success"] is True @@ -98,15 +96,15 @@ def test_send_response_success(): def test_send_response_error(): - """Debe devolver success=False y code=400 si el resultado es False.""" - resp, code = send_response(False, message="fallo") + """Should return success=False and code=400 if the result is False.""" + resp, code = send_response(False, message="failure") assert code == 400 assert resp["success"] is False assert resp["data"] is None - assert "fallo" in resp["error"] + assert "failure" in resp["error"] def ietf_intent(): - """Intento válido en formato IETF simplificado.""" + """Valid simplified IETF intent.""" return { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { @@ -136,13 +134,14 @@ def ietf_intent(): "id": "CU", "sdp-ip-address": "10.0.0.1", "service-match-criteria": { - "match-criterion": [{ + "match-criterion": [{ "match-type": [ { - "type": "vlan", - "vlan": [100] + "type": "vlan", + "vlan": [100] } - ]}] + ] + }] }, }, { @@ -151,12 +150,12 @@ def ietf_intent(): "service-match-criteria": { "match-criterion": [{ "match-type": [ - { + { "type": "vlan", "vlan": [100] - } - ] - }] + } + ] + }] }, }, ] @@ -168,7 +167,7 @@ def ietf_intent(): def test_build_response_ok(): - """Debe construir correctamente el response a partir de un intent IETF válido.""" + """Should correctly build the response from a valid IETF intent.""" intent = ietf_intent() response = [] result = build_response(intent, response) @@ -182,7 +181,7 @@ def test_build_response_ok(): assert slice_data["destination"] == "DU" assert slice_data["vlan"] == 100 - # Validar constraints + # Validate constraints requirements = slice_data["requirements"] assert any(r["constraint_type"] == "one-way-bandwidth[kbps]" and r["constraint_value"] == "1000" for r in requirements) assert any(r["constraint_type"] == "availability[%]" and r["constraint_value"] == "99.9" for r in requirements) @@ -190,7 +189,7 @@ def test_build_response_ok(): def test_build_response_empty_policy(): - """Debe devolver lista sin constraints si slo-policy está vacío.""" + """Should return a list without constraints if slo-policy is empty.""" intent = ietf_intent() intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"] = {} response = [] @@ -201,11 +200,11 @@ def test_build_response_empty_policy(): def test_build_response_invalid_intent(): - """Debe fallar limpiamente si el intent no tiene la estructura esperada.""" + """Should fail gracefully if the intent does not have the expected structure.""" bad_intent = {} response = [] try: result = build_response(bad_intent, response) except Exception: result = [] - assert result == [] + assert result == [] \ No newline at end of file -- GitLab From cd73c08df03ea36ff4d4a82fa0b4e1a99b783be5 Mon Sep 17 00:00:00 2001 From: armingol Date: Tue, 19 May 2026 15:20:15 +0000 Subject: [PATCH 13/27] feat: implement E2E optical planner integration and update deployment configuration --- src/config/.env.example | 4 +- src/config/config.py | 1 + .../e2e_optical_planner/e2e_optical.py | 57 +++++++++++++++++++ src/planner/planner.py | 3 + 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/planner/e2e_optical_planner/e2e_optical.py diff --git a/src/config/.env.example b/src/config/.env.example index 3460187..56af70f 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -31,12 +31,14 @@ 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 +# Type of planner to be used. Options: ENERGY, HRAT, TFS_OPTICAL, E2E_OPTICAL PLANNER_TYPE=ENERGY # HRAT HRAT_IP=10.0.0.1 # TFS_OPTICAL OPTICAL_PLANNER_IP=10.0.0.1 +# E2E_OPTICAL +E2E_OPTICAL_IP=127.0.0.1 # ------------------------- # Realizer diff --git a/src/config/config.py b/src/config/config.py index f5cb327..f4fd473 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -46,6 +46,7 @@ def create_config(app: Flask): 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") + app.config["E2E_OPTICAL_IP"] = os.getenv("E2E_OPTICAL_IP", "127.0.0.1") # Realizer app.config["DUMMY_MODE"] = os.getenv("DUMMY_MODE", "true").lower() == "true" diff --git a/src/planner/e2e_optical_planner/e2e_optical.py b/src/planner/e2e_optical_planner/e2e_optical.py new file mode 100644 index 0000000..a11068e --- /dev/null +++ b/src/planner/e2e_optical_planner/e2e_optical.py @@ -0,0 +1,57 @@ +# Copyright 2022-2026 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. + +import logging +import requests +import json +from src.utils.safe_get import safe_get + +def e2e_optical_planner(intent, ip: str, action: str = "create") -> dict: + """ + Plan E2E optical layer configuration using TFS NBI. + + Args: + intent (dict or str): Network slice intent + ip (str): IP address of the TFS NBI service + action (str, optional): Operation to perform - "create" or "delete". + Defaults to "create" + """ + if action == 'delete': + logging.debug("DELETE REQUEST RECEIVED FOR E2EOptical: %s", intent) + return None + + url = f"http://{ip}/restconf/e2epathcomp/v0/e2e_path_computation" + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + + payload = intent + + logging.debug(f"Sending request to E2EOptical Planner: {url}") + + try: + response = requests.post(url, headers=headers, json=payload, timeout=10) + + if response.status_code in [200, 201]: + response_data = response.json() + logging.info(f"E2EOptical Planner Response: {json.dumps(response_data, indent=2)}") + return response_data + else: + logging.warning(f"E2EOptical Planner returned status {response.status_code}: {response.text}") + return None + + except requests.exceptions.RequestException as e: + logging.warning(f"Error connecting to the E2EOptical Planner service: {e}") + return None diff --git a/src/planner/planner.py b/src/planner/planner.py index c856d0b..a8195fd 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -18,6 +18,7 @@ import logging from src.planner.energy_planner.energy import energy_planner from src.planner.hrat_planner.hrat import hrat_planner from src.planner.tfs_optical_planner.tfs_optical import tfs_optical_planner +from src.planner.e2e_optical_planner.e2e_optical import e2e_optical_planner from flask import current_app @@ -49,5 +50,7 @@ class Planner: elif type == "HRAT" : return hrat_planner(intent, current_app.config["HRAT_IP"]) # Use TFS optical planner with configured IP elif type == "TFS_OPTICAL": return tfs_optical_planner(intent, current_app.config["OPTICAL_PLANNER_IP"], action = "create") + # Use E2E optical planner with configured IP + elif type == "E2E_OPTICAL": return e2e_optical_planner(intent, current_app.config["E2E_OPTICAL_IP"], action = "create") # Return None if planner type is unsupported else : return None -- GitLab From 20cacd6fbb958b933d1833931d637a9d3f3ef65c Mon Sep 17 00:00:00 2001 From: armingol Date: Wed, 20 May 2026 07:07:36 +0000 Subject: [PATCH 14/27] code clean up --- src/config/.env.example | 4 +- src/config/config.py | 1 - src/planner/planner.py | 3 - .../tfs_optical_planner/tfs_optical.py | 393 ------------------ 4 files changed, 1 insertion(+), 400 deletions(-) delete mode 100644 src/planner/tfs_optical_planner/tfs_optical.py diff --git a/src/config/.env.example b/src/config/.env.example index 56af70f..1e45691 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -31,12 +31,10 @@ 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, E2E_OPTICAL +# Type of planner to be used. Options: ENERGY, HRAT, E2E_OPTICAL PLANNER_TYPE=ENERGY # HRAT HRAT_IP=10.0.0.1 -# TFS_OPTICAL -OPTICAL_PLANNER_IP=10.0.0.1 # E2E_OPTICAL E2E_OPTICAL_IP=127.0.0.1 diff --git a/src/config/config.py b/src/config/config.py index f4fd473..c059ede 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -45,7 +45,6 @@ def create_config(app: Flask): 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") app.config["E2E_OPTICAL_IP"] = os.getenv("E2E_OPTICAL_IP", "127.0.0.1") # Realizer diff --git a/src/planner/planner.py b/src/planner/planner.py index a8195fd..2a35d39 100644 --- a/src/planner/planner.py +++ b/src/planner/planner.py @@ -17,7 +17,6 @@ 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 src.planner.e2e_optical_planner.e2e_optical import e2e_optical_planner from flask import current_app @@ -48,8 +47,6 @@ class Planner: if type == "ENERGY" : return energy_planner(intent) # Use HRAT planner with configured IP elif type == "HRAT" : return hrat_planner(intent, current_app.config["HRAT_IP"]) - # Use TFS optical planner with configured IP - elif type == "TFS_OPTICAL": return tfs_optical_planner(intent, current_app.config["OPTICAL_PLANNER_IP"], action = "create") # Use E2E optical planner with configured IP elif type == "E2E_OPTICAL": return e2e_optical_planner(intent, current_app.config["E2E_OPTICAL_IP"], action = "create") # Return None if planner type is unsupported diff --git a/src/planner/tfs_optical_planner/tfs_optical.py b/src/planner/tfs_optical_planner/tfs_optical.py deleted file mode 100644 index 25e344a..0000000 --- a/src/planner/tfs_optical_planner/tfs_optical.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright 2022-2026 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: - """ - Plan optical layer configuration for TeraFlow SDN network slices. - - This function computes optical paths and generates configuration rules for - point-to-multipoint (P2MP) optical connections, including transceiver - activation and Layer 3 VPN configuration. - - Args: - intent (dict or str): For create action - network slice intent with service - delivery points. For delete action - slice ID string - ip (str): IP address of the optical path computation service - action (str, optional): Operation to perform - "create" or "delete". - Defaults to "create" - - Returns: - dict or None: Configuration rules containing: - - network-slice-uuid: Unique identifier - - viability: Boolean indicating success - - actions: List of provisioning actions for: - * XR_AGENT_ACTIVATE_TRANSCEIVER (optical layer) - * CONFIG_VPNL3 (IP layer) - Returns None if source/destination not found or service unavailable - - Notes: - - Supports P2MP (Point-to-Multipoint) connectivity - - Computes optical paths using external TFS optical service - - Configures digital subcarrier groups for wavelength division - - Port 31060 used for optical path computation API - - Raises: - requests.exceptions.RequestException: On connection errors (logged, returns None) - """ - if action == 'delete': - logging.debug("DELETE REQUEST RECEIVED: %s", intent) - # Load slice database to retrieve intent for deletion - 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'] - - # Extract source and destination from P2MP structure - 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: - # Extract source and destination from creation intent - 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): - """ - Send path computation request to the optical TFS service. - - Computes point-to-multipoint optical paths using the TAPI path computation API. - - Args: - source (str or list): Root node identifier(s) for P2MP path - destination (str or list): Leaf node identifier(s) for P2MP path - ip (str): IP address of the TFS optical service - - Returns: - dict or None: Path computation response containing connectivity service - with optical connection attributes, or None on failure - - Notes: - - API endpoint: POST /OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp - - Assumes 100 Gbps bitrate, bidirectional transmission - - Band width of 200, with 4 subcarriers per source - - 15 second timeout for requests - """ - url = f"http://{ip}:31060/OpticalTFS/restconf/operations/tapi-path-computation:compute-p2mp" - - headers = { - "Content-Type": "application/json", - "Accept": "*/*" - } - - # Normalize source and destination to lists - 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=1) - 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): - """ - Generate a digital subcarrier group configuration block. - - Creates configuration for optical digital subcarriers, which are used for - wavelength division multiplexing in optical networks. - - Args: - group (dict): Subcarrier group data from path computation response - action (str): "create" to activate, "delete" to deactivate - group_id_override (int, optional): Override group ID. Defaults to None - node (str, optional): Node type - "leaf" for simplified config. Defaults to None - - Returns: - dict: Digital subcarrier group configuration with: - - digital_sub_carriers_group_id: Group identifier - - digital_sub_carrier_id: List of subcarrier configs with active status - - Notes: - - Leaf nodes use fixed 4 subcarriers (IDs 1-4) - - Non-leaf nodes use subcarrier IDs from computation response - """ - 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": - # Simplified configuration for leaf nodes - 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: - # Full configuration based on computed path - 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): - """ - Generate provisioning rules for optical and IP layer configuration. - - Transforms path computation results into concrete configuration actions - for transceivers and Layer 3 VPN setup. - - Args: - connectivity_service (dict): Path computation summary containing: - - source: Root node identifier - - destination: List of leaf node identifiers - - connectivity-service: Optical connection attributes - intent (dict): Original network slice intent with IP configuration - action (str): "create" or "delete" operation - - Returns: - list: Configuration rules with provisioning actions - - Notes: - - For create: Generates XR_AGENT_ACTIVATE_TRANSCEIVER and CONFIG_VPNL3 actions - - For delete: Generates DEACTIVATE_XR_AGENT_TRANSCEIVER actions - - Hub node uses channel-1 at 195000000 MHz - - Leaf nodes assigned specific channels (channel-1, channel-3, channel-5) - - Fixed VLAN ID of 500 for all connections - - Tunnel UUID generated from source-destination string - """ - src_name = connectivity_service.get("source", "FALTA VALOR") - dest_list = connectivity_service.get("destination", ["FALTA VALOR"]) - dest_str = ",".join(dest_list) - config_rules = [] - - # Generate deterministic UUID for tunnel based on endpoints - 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": [] - } - - # Extract optical connection attributes from path computation - 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"] - - # Build hub (root) configuration with all subcarrier groups - 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 - } - - # Build leaf configurations with specific frequencies per destination - leaves = [] - for dest, group in zip(connectivity_service["destination"], groups): - # Map destinations to specific channels and frequencies - 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': - # Add transceiver activation action - provisionamiento["actions"].append({ - "type": "XR_AGENT_ACTIVATE_TRANSCEIVER", - "layer": "OPTICAL", - "content": final_json, - "controller-uuid": "IPoWDM Controller" - }) - - # Extract IP configuration from intent for L3 VPN setup - 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 # Fixed VLAN ID - nodes[node] = { - "ip-address": ip, - "ip-mask": prefix, - "vlan-id": vlan - } - - # Add L3 VPN configuration action for P2MP topology - 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: - # For deletion, generate deactivation action - 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 \ No newline at end of file -- GitLab From cd71d9ccb731a58ed38626f405b1f33110a78d1f Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 21 May 2026 10:56:28 +0000 Subject: [PATCH 15/27] feat: Copy .env if not exists --- deploy.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/deploy.sh b/deploy.sh index c58d17d..d47a19d 100755 --- a/deploy.sh +++ b/deploy.sh @@ -43,9 +43,14 @@ if [ ! -f src/config/.env.example ]; then exit 1 fi -# Copy .env.example to .env -echo "Generating .env file..." -cp src/config/.env.example .env +# Copy .env if not exists +if [ -f src/config/.env ]; then + echo "Using existing src/config/.env file..." + cp src/config/.env .env +else + echo "Generating .env file from example..." + cp src/config/.env.example .env +fi # Read NSC_PORT from .env NSC_PORT=$(grep '^NSC_PORT=' .env | cut -d '=' -f2) -- GitLab From dd75adeabe648d86d68a532e25f98df13ebde150 Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:23 +0200 Subject: [PATCH 16/27] Monitor System v1.0.0 --- PM Collections/NSC.postman_collection.json | 448 +++++++++++++++++ PM Collections/NSC.postman_environment.json | 28 ++ app.py | 3 +- requirements.txt | 4 +- src/api/main.py | 517 +++++++++++++++++++- swagger/restconf_namespace.py | 152 +++++- 6 files changed, 1145 insertions(+), 7 deletions(-) create mode 100644 PM Collections/NSC.postman_collection.json create mode 100644 PM Collections/NSC.postman_environment.json diff --git a/PM Collections/NSC.postman_collection.json b/PM Collections/NSC.postman_collection.json new file mode 100644 index 0000000..08f9531 --- /dev/null +++ b/PM Collections/NSC.postman_collection.json @@ -0,0 +1,448 @@ +{ + "info": { + "_postman_id": "43660461-a738-488b-a6f5-2c244a9cae25", + "name": "NSC", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "45361142", + "_collection_link": "https://go.postman.co/collection/45361142-43660461-a738-488b-a6f5-2c244a9cae25?source=collection_link" + }, + "item": [ + { + "name": "Operations", + "item": [ + { + "name": "Clients", + "item": [ + { + "name": "Create Client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Get All Clients", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients" + ] + } + }, + "response": [] + }, + { + "name": "Delete Client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/clients/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "clients", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Subscriptions", + "item": [ + { + "name": "Slices", + "item": [ + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Slice Telemetry by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"frequency\": 2\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "All subscriptions of a client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "All subscriptions of a client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "subscriptions", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Stream", + "item": [ + { + "name": "Stream All Telemetry Subscriptions by client", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "stream", + "{{CLIENT_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Stream Telemetry Subscription by client Copy", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "stream", + "{{CLIENT_ID}}", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Telemetry of a Slice", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/telemetry/operations/slices/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "telemetry", + "operations", + "slices", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/PM Collections/NSC.postman_environment.json b/PM Collections/NSC.postman_environment.json new file mode 100644 index 0000000..19c9e3b --- /dev/null +++ b/PM Collections/NSC.postman_environment.json @@ -0,0 +1,28 @@ +{ + "id": "fb33daed-2c4b-4c85-a9b2-4d338123ed0b", + "name": "NSC", + "values": [ + { + "key": "HOST", + "value": "http://172.17.0.2:8086/nsc", + "type": "default", + "enabled": true + }, + { + "key": "CLIENT_ID", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "SLICE_ID", + "value": "", + "type": "default", + "enabled": true + } + ], + "color": null, + "_postman_variable_scope": "environment", + "_postman_exported_at": "2026-03-27T10:00:42.609Z", + "_postman_exported_using": "Postman/12.3.5" +} \ No newline at end of file diff --git a/app.py b/app.py index d3f1706..2104af8 100644 --- a/app.py +++ b/app.py @@ -60,6 +60,7 @@ def create_app(): init_service() app = Flask(__name__) app = create_config(app) + app.logger.setLevel(logging.INFO) CORS(app) # Configure logging to provide clear and informative log messages @@ -104,4 +105,4 @@ def create_app(): if __name__ == "__main__": app = create_app() - app.run(host="0.0.0.0", port=NSC_PORT, debug=True) \ No newline at end of file + app.run(host="0.0.0.0", port=NSC_PORT, debug=True, threaded=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6fde3e4..d8af18e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -Flask +flask[async] flask-cors flask-restx netmiko @@ -25,4 +25,4 @@ coverage pytest libyang==3.3.0 sysrepo==1.7.6 - +aiohttp \ No newline at end of file diff --git a/src/api/main.py b/src/api/main.py index eae484a..7934dd9 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -14,6 +14,11 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. +import json +import time +from typing import Dict, List, Tuple +import uuid + from src.utils.send_response import send_response import logging from flask import current_app @@ -22,10 +27,26 @@ from src.database.service_db import delete_by_slice_id, get_data_by_slice_id from src.realizer.tfs.helpers.tfs_connector import tfs_connector 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 collections import deque +import traceback + +import asyncio +import aiohttp class Api: + NSC_BASE_URL = "http://192.168.27.165" + SDN_TOPOLOGY_URL = f"{NSC_BASE_URL}/restconf/data/ietf-network:networks" + SDN_SUBSCRIBE_URL = f"{NSC_BASE_URL}/restconf/operations/subscriptions:establish-subscription" + SDN_SUBSCRIPTION_PERIOD = 10 # seconds + SDN_SUBS_INACTIVITY_THRESHOLD = 30 # seconds + def __init__(self, slice_service): self.slice_service = slice_service + if "slices" not in current_app.config: current_app.config["slices"] = {} + if "telemetryClients" not in current_app.config: current_app.config["telemetryClients"] = {} + if "slices" not in current_app.config: current_app.config["slices"] = {} + self.telemetry_cache = {} + def add_flow(self, intent): """ @@ -303,6 +324,11 @@ class Api: result = self.slice_service.nsc(intent) if result: create_data_store(intent) + for slice in result["slices"]: + current_app.config["slices"][slice["id"]] = { + "src": slice["source"], + "dst": slice["destination"] + } logging.info(f"Network Slice created successfully") return send_response( True, @@ -628,6 +654,7 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -684,6 +711,7 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].pop(slice_id, None) logging.info(f"Slice {slice_id} removed successfully") return {}, 204 @@ -706,6 +734,7 @@ class Api: if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) + current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -744,4 +773,490 @@ class Api: except ValueError as e: return send_response(False, code=404, message=str(e)) except Exception as e: - return send_response(False, code=500, message=str(e)) \ No newline at end of file + return send_response(False, code=500, message=str(e)) + + def getShortestPath(self, network, srcNodeId, dstNodeId, directedGraph: bool = False) -> Tuple[Dict[str, any], int]: + try: + sourceNodeIdx = srcNodeId + destNodeIdx = dstNodeId + nodes = network["node"] + links = network["ietf-network-topology:link"] + + # Construir grafo (no dirigido) + graph = {node["node-id"]: set() for node in nodes} + for link in links: + src = link["source"]["source-node"] + dst = link["destination"]["dest-node"] + + graph[src].add(dst) + if not directedGraph: graph[dst].add(src) + + # BFS + visited = { node["node-id"]: False for node in nodes } + prev = { node["node-id"]: -1 for node in nodes } + + queue = deque([sourceNodeIdx]) + visited[sourceNodeIdx] = True + + while queue: + current = queue.popleft() + if current == destNodeIdx: break + + for neighbor in graph[current]: + if not visited[neighbor]: + visited[neighbor] = True + prev[neighbor] = current + queue.append(neighbor) + + # Reconstruir camino + path = [] + at = destNodeIdx + if not visited[destNodeIdx]: return {"message": "No path found"}, 404 + + while at != -1: + path.append(at) + at = prev[at] + path.reverse() + return path, 200 + except Exception as e: + logging.info(f"Error calculating shortest path: {traceback.print_exc()}") + return {"message": str(e)}, 500 + + # --- SDN STREAMS --- + + async def listenStream(self, session, link_id, slice_id, stream_url): + async with session.get(stream_url) as resp: + buffer = "" + + logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") + async for chunk in resp.content: + buffer += chunk.decode() + + try: + # intenta parsear un JSON completo + data, index = json.JSONDecoder().raw_decode(buffer) + logging.info(f"Received telemetry for link {link_id} in slice {slice_id}: {data}") + buffer = buffer[index:].lstrip() + + telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + + self.telemetry_cache.setdefault(slice_id, {})[link_id] = { + "timestamp": data["notification"]["eventTime"], #time.time(), + "bandwidth": float(telemetry.get("bandwidth")), + "latency": float(telemetry.get("latency")), + "services": telemetry.get("services", []) + } + buffer = "" + except json.JSONDecodeError: pass # aún no hay suficiente data + logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") + + async def startStreams(self, links, slice_id): + async with aiohttp.ClientSession(timeout = aiohttp.ClientTimeout(total=10)) as session: + tasks = [] + + for link in links: + sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ + "ietf-subscribed-notifications:input": { + "datastore": "operational", + "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", + "ietf-yang-push:periodic": { + "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD + } + } + }, + auth=aiohttp.BasicAuth("admin", "admin") + ) + sub_data = await sub_resp.json() + logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") + """ + response format is: + { + "id": 1161739417341820929, + "uri": "/restconf/stream/1161739417341820929" + } + """ + + if "uri" in sub_data: + tasks.append( + asyncio.create_task( + self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) + ) + ) + await asyncio.gather(*tasks) + + # --- CLIENTS --- + + def createClient(self, clientId): + response, code = self.getClient(clientId) + if code == 400: return response, code + if code == 200: return {"message": f"Client '{clientId}' already exists"}, 400 + + current_app.config["telemetryClients"][clientId] = { "subscriptions": {} } + return {"message": f"Client '{clientId}' created successfully"}, 200 + + def deleteClient(self, clientId): + response, code = self.getClient(clientId) + if response != 200: return response, code + + del current_app.config["telemetryClients"][clientId] + return {"message": f"Client '{clientId}' deleted successfully"}, 200 + + def getClient(self, clientId) -> Tuple[Dict[str,any], int]: + if not clientId: return {"message": "clientId is required"}, 400 + clients = current_app.config["telemetryClients"] + + if clientId not in clients: return {"message": f"Client '{clientId}' not found"}, 404 + return clients[clientId], 200 + + def getAllClients(self) -> Tuple[Dict[str,any], int]: return current_app.config["telemetryClients"], 200 + + # --- TELEMETRY --- + + async def getNetworkTopology(self, sliceId) -> Tuple[Dict[str, any], int]: + # MOCKED TOPOLOGY + #topology = requests.get(Api.SDN_TOPOLOGY_URL) + topology = [ + { + "ietf-network:networks": { + "network": [ + { + "ietf-network-topology:link": [ + { + "destination": { + "dest-node": "AMF-N2", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "4.4.4.4-AMF-N2", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/2-GigabitEthernet0/0/0/2" + } + }, + { + "destination": { + "dest-node": "1.1.1.1", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "4.4.4.4-1.1.1.1", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "3.3.3.3", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "CU-N2-3.3.3.3", + "source": { + "source-node": "CU-N2", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "1.1.1.1-4.4.4.4", + "source": { + "source-node": "1.1.1.1", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "CU-N2", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "1.1.1.1-CU-N2", + "source": { + "source-node": "1.1.1.1", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/2-GigabitEthernet0/0/0/2" + }, + "link-id": "AMF-N2-4.4.4.4", + "source": { + "source-node": "AMF-N2", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "CU-N2", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "3.3.3.3-CU-N2", + "source": { + "source-node": "3.3.3.3", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "destination": { + "dest-node": "4.4.4.4", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "3.3.3.3-4.4.4.4", + "source": { + "source-node": "3.3.3.3", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "3.3.3.3", + "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" + }, + "link-id": "4.4.4.4-3.3.3.3", + "source": { + "source-node": "4.4.4.4", + "source-tp": "0/0/1-GigabitEthernet0/0/0/1" + } + }, + { + "destination": { + "dest-node": "1.1.1.1", + "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" + }, + "link-id": "CU-N2-1.1.1.1", + "source": { + "source-node": "CU-N2", + "source-tp": "0/0/0-GigabitEthernet0/0/0/0" + } + } + ], + "network-id": "slice_1 MOCKED", + "network-types": { + "ietf-l3-unicast-topology:l3-unicast-topology": {} + }, + "node": [ + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "CU-N2" + }, + "node-id": "CU-N2" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "AMF-N2" + }, + "node-id": "AMF-N2" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "1.1.1.1" + }, + "node-id": "1.1.1.1" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "4.4.4.4" + }, + "node-id": "4.4.4.4" + }, + { + "ietf-l3-unicast-topology:l3-node-attributes": { + "name": "3.3.3.3" + }, + "node-id": "3.3.3.3" + } + ] + } + ] + } + } + ] if True else (await response.json()) + + network = topology[0]["ietf-network:networks"]["network"] + network = network[0] #next((n for n in network if n["network-id"] == sliceId), None) + return network, 200 + + async def getTelemetry(self, sliceId, subscribe: bool = False) -> Dict[str, any]: + if sliceId in current_app.config["slices"]: + #Retrieve Network topology: + config = current_app.config["slices"][sliceId] + if "topology" not in config or config["topology"] is None: + topology, code = await self.getNetworkTopology(sliceId) #Currently mocked + if code != 200: return { "message": f"There was an error retrieving topology for slice '{sliceId}': {topology}" } + config["topology"] = topology + else: topology = config["topology"] + + #Get shortest path from source node to destination node: + if "shortestPath" not in config or config["shortestPath"] is None: + shortestPath, code = self.getShortestPath(topology, config["src"], config["dst"]) + logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + if code != 200: return shortestPath + config["shortestPath"] = shortestPath + else: shortestPath = config["shortestPath"] + logging.info(f"[getTelemetry] Shortest path for slice '{sliceId}': {shortestPath}") + + # Make sure data is already initialized in cache + # (this will only happen if an already-opened stream with SDN controller has sent data for this slice, + # otherwise it will be initialized when the first data arrives): + if sliceId in self.telemetry_cache: + """ the format is the following: + self.telemetry_cache[sliceId][linkId] = { + "timestamp": str, #time.time(), + "bandwidth": float, + "latency": float, + "services": List[any] + } + """ + return { + "sliceId": sliceId, + "telemetry": { + "desc": f"Aggregated telemetry of a Network slice {sliceId}. Other additional fields should be included in the real version.", + "latency": f"{sum([link.latency for key, link in self.telemetry_cache[sliceId].items()])}ms", + "bandwidth": f"{sum([link.bandwidth for key, link in self.telemetry_cache[sliceId].items()])}Mb/s" + }, + "timestamp": time.time() + } + else: + if subscribe: # Open a stream with SDN controller for this slice: + # Get telemetry of every node in the slice: + """ + self.startStreams(self, [ + link["link-id"] + for link in topology["ietf-network-topology:link"] + ], sliceId) + """ + + # Get telemetry only of the links in the shortest path: + await self.startStreams([ f"{shortestPath[i]}-{shortestPath[i+1]}" for i in range(len(shortestPath)-1) ], sliceId) + return { "message": f"No telemetry available for slice '{sliceId}'" } + else: return { "message": f"There is no slice with id '{sliceId}' registered" } + + async def getAllTelemetry(self, subscribe: bool = False) -> Dict[str, Dict[str, any]]: + telemetryData = {} + for sliceId in current_app.config.get("slices", {}).keys(): telemetryData[sliceId] = await self.getTelemetry(sliceId, subscribe) + return telemetryData + + # --- SUBSCRIPTIONS --- + + async def getSubscriptionJson(self, response, sliceId, subscribe: bool = False): + return { + "subscriptionFrequency": response["frequency"], + "sourceNode": current_app.config["slices"][sliceId]["src"] if sliceId in current_app.config["slices"] else "UNKNOWN", + "destinationNode": current_app.config["slices"][sliceId]["dst"] if sliceId in current_app.config["slices"] else "UNKNOWN", + "telemetry": await self.getTelemetry(sliceId, subscribe) + } + + async def getTelemetrySubscriptions(self, clientId, sliceId = None, subscribe: bool = False) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + if sliceId is not None: + if sliceId not in response["subscriptions"]: return {"message": f"Client '{clientId}' has no subscription for slice '{sliceId}'"}, 400 + + return { + "clientId": clientId, + "sliceSubscription": await self.getSubscriptionJson(response["subscriptions"][sliceId], sliceId, subscribe) + }, 200 + else: + return { + "clientId": clientId, + "sliceSubscriptions": [ (await self.getSubscriptionJson(sub, sId, subscribe)) for sId, sub in response["subscriptions"].items() ] + }, 200 + + async def createTelemetrySubscription(self, clientId, sliceId, frequency, src, dst) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + sliceExists = sliceId in response["subscriptions"] + + if not frequency: return {"message": "Field 'frequency' is required"}, 400 + + topology, code = await self.getNetworkTopology(sliceId) #Currently mocked + if code != 200: + msg = f"There was an error retrieving topology for slice '{sliceId}': {topology}" + logging.error(msg) + return { "message": msg }, code + + #Get shortest path from source node to destination node: + shortestPath, code = self.getShortestPath(topology, src, dst) + if code != 200: + logging.info(f"Error while computing shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + return shortestPath, code + else: + logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") + + response["subscriptions"][sliceId] = { "frequency": frequency } + current_app.config["slices"][sliceId] = { + "src": src, + "dst": dst, + "topology": topology, + "shortestPath": shortestPath + } + + return { + "message": f"Subscription successfully {"modified" if sliceExists else "created"}", + "sliceId": sliceId, + "frequency": frequency #"clientId": str(uuid.uuid4()) + }, 201 + + def deleteTelemetrySubscriptions(self, clientId, sliceId = None) -> Tuple[Dict[str,any], int]: + response, code = self.getClient(clientId) + if code != 200: return response, code + if sliceId: + if sliceId not in response["subscriptions"]: return {"message": f"Subscription '{sliceId}' not found for client {clientId}"}, 404 + + del response["subscriptions"][sliceId] + return {"message": "Subscription deleted"}, 200 + else: + response["subscriptions"] = {} + return {"message": "All subscriptions deleted"}, 200 + + def sync_stream(self, async_gen_func, *args, **kwargs): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + agen = async_gen_func(*args, **kwargs) + + try: + while True: yield loop.run_until_complete(agen.__anext__()) + except StopAsyncIteration: pass + finally: loop.close() + + async def openSubscriptions(self, clientId, sliceId = None): + try: + response, code = self.getClient(clientId) + if code != 200: yield f"event: error\ndata: {json.dumps(data)}\n\n" + else: + frequency = 0 + okToProceed = True + # Process only for one slice: + if sliceId: + if sliceId in response["subscriptions"]: + frequency = response["subscriptions"][sliceId]["frequency"] + yield f"data: Using frequency of {frequency}...\n\n" + else: + yield f"data: sliceId '{sliceId}' is not present within '{clientId}' subscriptions. Closing connection now...\n\n" + okToProceed = False + # Process for all slices: + else: + # Using policy of highest frequency among the subscribed slices, but other policies could be implemented (e.g., average/min frequency, one stream per slice with its own frequency, etc.) + frequency = max([ sub["frequency"] for _, sub in response["subscriptions"].items() ]) if response["subscriptions"] else 0 + #for sliceId, sub in response["subscriptions"].items(): frequency = max(frequency, sub["frequency"]) + yield f"data: Using frequency of {frequency}...\n\n" + + while okToProceed: + try: + data, code = await self.getTelemetrySubscriptions(clientId, sliceId, True) + if code == 200: + yield f"data: {json.dumps(data)}\n\n" + await asyncio.sleep(frequency) + else: + yield f"event: error\ndata: {json.dumps(data)}\n\n" + break + except aiohttp.ClientError as e: + errorStr = "Connection timeout to SDN Controller." if "Connection timeout to host" in str(e) else str(e) + yield f"event: error\ndata: {json.dumps({'error': errorStr})}\n\n" + break + except GeneratorExit: + current_app.logger.info(traceback.print_exc()) + #current_app.config["telemetryClients"].pop(clientId, None) + except Exception: current_app.logger.info(traceback.print_exc()) \ No newline at end of file diff --git a/swagger/restconf_namespace.py b/swagger/restconf_namespace.py index 9682947..aa7e18a 100644 --- a/swagger/restconf_namespace.py +++ b/swagger/restconf_namespace.py @@ -14,8 +14,13 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -from flask import request -from flask_restx import Namespace, Resource, reqparse +import logging +import time +import json +import uuid + +from flask import request, Response, request, current_app, stream_with_context +from flask_restx import Namespace, Resource, fields, reqparse from src.main import NSController from src.api.main import Api from swagger.models.create_models_restconf import create_ietf_network_slice_nbi_yang_model @@ -211,4 +216,145 @@ class Sdp(Resource): @restconf_ns.response(204, "SDP deleted") def delete(self, slice_service_id, sdp_id): controller = NSController(controller_type="RESTCONF") - return Api(controller).delete_sdps(slice_service_id, sdp_id) \ No newline at end of file + return Api(controller).delete_sdps(slice_service_id, sdp_id) + +# ================================================================================================================================================ +@restconf_ns.route("/telemetry/operations/clients") +class TelemetryAllClientsRequest(Resource): + @restconf_ns.doc(description="Gets the list of all registered clients") + @restconf_ns.response(200, "Clients retrieved") + def get(self): + clients, code = Api(NSController(controller_type="RESTCONF")).getAllClients() + return [clientName for clientName, clientData in clients.items()], code + +@restconf_ns.route("/telemetry/operations/clients/") +class TelemetryClientRequest(Resource): + @restconf_ns.doc(description="Creates a new client") + @restconf_ns.response(200, "Client created") + def post(self, clientId): return Api(NSController(controller_type="RESTCONF")).createClient(clientId) + + @restconf_ns.doc(description="Deletes a given client") + @restconf_ns.response(200, "Client deleted") + def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteClient(clientId) + + +@restconf_ns.route("/telemetry/operations/slices") +class TelemetrySliceRequest(Resource): + @restconf_ns.doc(description="Gets latest telemetry of a certain network slice, or of all slices.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, retrieves all slices.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(200, "Telemetry retrieved") + @restconf_ns.response(404, "Slice not found") + def get(self): + api = Api(NSController(controller_type="RESTCONF")) + sliceId = request.args.get("sliceId", None) + + ret = {} + if sliceId: ret = current_app.ensure_sync(api.getTelemetry)(sliceId) + else: ret = current_app.ensure_sync(api.getAllTelemetry)() + + logging.info(f"Telemetry retrieved successfully for sliceId={sliceId if sliceId else 'ALL'}: {ret}") + return ret, 200 + +@restconf_ns.route("/telemetry/operations/stream/") +class TelemetryStreamRequest(Resource): + @restconf_ns.doc( + description="Opens a telemetry stream of a given subscription (or all of them) of a certain client", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, streams all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Telemetry opened") + @restconf_ns.response(400, "Invalid request") + def get(self, clientId): + api = Api(NSController(controller_type="RESTCONF")) + return Response( + stream_with_context( + api.sync_stream( + api.openSubscriptions, + clientId, + request.args.get("sliceId", None) + ) + ), + mimetype="text/event-stream" + ) + + +@restconf_ns.route("/telemetry/operations/slice-subscriptions/") +class TelemetrySubscriptionsRequest(Resource): + @restconf_ns.doc( + description="Retrieves a specific telemetry subscription (or all of them) of a given client.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, retrieves all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription(s) successfully retrieved") + @restconf_ns.response(400, "Invalid request") + def get(self, clientId): + api = Api(NSController(controller_type="RESTCONF")) + return current_app.ensure_sync(api.getTelemetrySubscriptions)(clientId, request.args.get("sliceId", None), False) + + @restconf_ns.doc( + description="Deletes a specific telemetry subscription (or all of them) of a given client.", + params={ + "sliceId": { + "description": "Network Slice ID (optional). If not provided, deletes all subscriptions.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription(s) successfully deleted") + @restconf_ns.response(400, "Invalid request") + def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteTelemetrySubscriptions(clientId, request.args.get("sliceId", None)) + + @restconf_ns.doc( + description="Creates a new telemetry subscription for a given client.", + params={ + "sliceId": { + "description": "Network Slice ID. If not provided, the sliceId must be included in the request body.", + "in": "query", + "required": False, + "type": "string" + } + } + ) + @restconf_ns.response(201, "Subscription successfully created") + @restconf_ns.response(400, "Invalid request") + @restconf_ns.expect(restconf_ns.model("TelemetrySubscriptionModel", { + "sliceId": fields.String(required=False, description="Network Slice ID (if not provided as query param)"), + "frequency": fields.Integer(required=True, description="Telemetry push frequency in seconds"), + "src": fields.String(required=True, description="Source Node Id"), + "dst": fields.String(required=True, description="Destination Node Id") + }), validate=True) + def post(self, clientId): + sliceId = request.args.get("sliceId", None) + if not sliceId: sliceId = request.json.get("sliceId", None) + if not sliceId: return {"error": "sliceId query param is required"}, 400 + + api = Api(NSController(controller_type="RESTCONF")) + return current_app.ensure_sync(api.createTelemetrySubscription)( + clientId, + sliceId, + request.json.get("frequency", None), + request.json.get("src", None), + request.json.get("dst", None), + ) \ No newline at end of file -- GitLab From de12470936d457109dd368bda3e70870a5924c36 Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:34 +0200 Subject: [PATCH 17/27] NSC Monitor system v1.0.0 --- src/api/main.py | 169 ++++++++++++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 62 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index 7934dd9..f051ebf 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -19,6 +19,8 @@ import time from typing import Dict, List, Tuple import uuid +import requests + from src.utils.send_response import send_response import logging from flask import current_app @@ -34,7 +36,7 @@ import asyncio import aiohttp class Api: - NSC_BASE_URL = "http://192.168.27.165" + NSC_BASE_URL = "http://192.168.27.189" SDN_TOPOLOGY_URL = f"{NSC_BASE_URL}/restconf/data/ietf-network:networks" SDN_SUBSCRIBE_URL = f"{NSC_BASE_URL}/restconf/operations/subscriptions:establish-subscription" SDN_SUBSCRIPTION_PERIOD = 10 # seconds @@ -46,8 +48,17 @@ class Api: if "telemetryClients" not in current_app.config: current_app.config["telemetryClients"] = {} if "slices" not in current_app.config: current_app.config["slices"] = {} self.telemetry_cache = {} + self.session = None #For connections with SDN - + async def get_session(self): + # Crea la sesión si no existe + if self.session is None or self.session.closed: self.session = aiohttp.ClientSession() + return self.session + + async def close_session(self): + # Llama a esto al apagar tu app + if self.session: await self.session.close() + def add_flow(self, intent): """ Create a new transport network slice. @@ -825,64 +836,82 @@ class Api: # --- SDN STREAMS --- async def listenStream(self, session, link_id, slice_id, stream_url): - async with session.get(stream_url) as resp: - buffer = "" - - logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") - async for chunk in resp.content: - buffer += chunk.decode() - - try: - # intenta parsear un JSON completo - data, index = json.JSONDecoder().raw_decode(buffer) - logging.info(f"Received telemetry for link {link_id} in slice {slice_id}: {data}") - buffer = buffer[index:].lstrip() - - telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] - - self.telemetry_cache.setdefault(slice_id, {})[link_id] = { - "timestamp": data["notification"]["eventTime"], #time.time(), - "bandwidth": float(telemetry.get("bandwidth")), - "latency": float(telemetry.get("latency")), - "services": telemetry.get("services", []) - } - buffer = "" - except json.JSONDecodeError: pass # aún no hay suficiente data + try: + async with session.get( + stream_url, + auth=aiohttp.BasicAuth("admin", "admin"), + timeout=None, + headers = { + "Accept": "text/event-stream", + "Cache-Control": "no-cache" + } + ) as resp: + buffer = "" + + logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") + + async for chunk in resp.content.iter_any(): + buffer += chunk.decode() + + # SSE separa los eventos con dos saltos de línea (\n\n) + while "\n\n" in buffer: + # Extraemos el evento completo y dejamos el resto en el buffer + event_block, buffer = buffer.split("\n\n", 1) + + for line in event_block.split("\n"): + if line.startswith("data:"): + json_str = line.replace("data:", "").strip() + + try: + data = json.loads(json_str) + logging.info(f"JSON detected: {data}") + + telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + self.telemetry_cache.setdefault(slice_id, {})[link_id] = { + "timestamp": data["notification"]["eventTime"], + "bandwidth": float(telemetry.get("bandwidth-utilization", 0)), + "latency": float(telemetry.get("latency", 0)), + "services": telemetry.get("related-service-ids", []) + } + except json.JSONDecodeError: logging.error("Error parsing the JSON from field 'data'") + except Exception as e: logging.error(f"Error processing logic: {e}") + except aiohttp.ClientError as e: logging.error(f"Connection error with stream for link {link_id}: {e}") logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") async def startStreams(self, links, slice_id): - async with aiohttp.ClientSession(timeout = aiohttp.ClientTimeout(total=10)) as session: - tasks = [] - - for link in links: - sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ - "ietf-subscribed-notifications:input": { - "datastore": "operational", - "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", - "ietf-yang-push:periodic": { - "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD - } + #async with self.get_session() as session: #timeout = aiohttp.ClientTimeout(total=10) + session = await self.get_session() + tasks = [] + + for link in links: + sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ + "ietf-subscribed-notifications:input": { + "datastore": "operational", + "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", + "ietf-yang-push:periodic": { + "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD } - }, - auth=aiohttp.BasicAuth("admin", "admin") - ) - sub_data = await sub_resp.json() - logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") - """ - response format is: - { - "id": 1161739417341820929, - "uri": "/restconf/stream/1161739417341820929" } - """ - - if "uri" in sub_data: - tasks.append( - asyncio.create_task( - self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) - ) + }, + auth=aiohttp.BasicAuth("admin", "admin") + ) + sub_data = await sub_resp.json() + logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") + """ + response format is: + { + "id": 1161739417341820929, + "uri": "/restconf/stream/1161739417341820929" + } + """ + + if "uri" in sub_data: + tasks.append( + asyncio.create_task( + self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) ) - await asyncio.gather(*tasks) + ) + #await asyncio.gather(*tasks) # --- CLIENTS --- @@ -914,7 +943,10 @@ class Api: async def getNetworkTopology(self, sliceId) -> Tuple[Dict[str, any], int]: # MOCKED TOPOLOGY - #topology = requests.get(Api.SDN_TOPOLOGY_URL) + topology = requests.get(Api.SDN_TOPOLOGY_URL, auth=("admin", "admin")) + networkRaw = topology.json() + + """ topology = [ { "ietf-network:networks": { @@ -1073,9 +1105,11 @@ class Api: } } ] if True else (await response.json()) + #""" + + network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == sliceId), None) + logging.info(f"Retrieved topology for slice '{sliceId}': {network}") - network = topology[0]["ietf-network:networks"]["network"] - network = network[0] #next((n for n in network if n["network-id"] == sliceId), None) return network, 200 async def getTelemetry(self, sliceId, subscribe: bool = False) -> Dict[str, any]: @@ -1095,7 +1129,6 @@ class Api: if code != 200: return shortestPath config["shortestPath"] = shortestPath else: shortestPath = config["shortestPath"] - logging.info(f"[getTelemetry] Shortest path for slice '{sliceId}': {shortestPath}") # Make sure data is already initialized in cache # (this will only happen if an already-opened stream with SDN controller has sent data for this slice, @@ -1112,14 +1145,26 @@ class Api: return { "sliceId": sliceId, "telemetry": { - "desc": f"Aggregated telemetry of a Network slice {sliceId}. Other additional fields should be included in the real version.", - "latency": f"{sum([link.latency for key, link in self.telemetry_cache[sliceId].items()])}ms", - "bandwidth": f"{sum([link.bandwidth for key, link in self.telemetry_cache[sliceId].items()])}Mb/s" + "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields should be included in the real version.", + "latency": f"{sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()])}ms", + "bandwidth": f"{sum([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()])}Mb/s" }, "timestamp": time.time() } else: if subscribe: # Open a stream with SDN controller for this slice: + shortestPathLinks = [] + if sliceId not in self.telemetry_cache: self.telemetry_cache[sliceId] = {} + for i in range(len(shortestPath)-1): + linkId = f"{shortestPath[i]}-{shortestPath[i+1]}" + shortestPathLinks.append(linkId) + self.telemetry_cache[sliceId][linkId] = { + "timestamp": None, #time.time(), + "bandwidth": 0, + "latency": 0, + "services": [] + } + # Get telemetry of every node in the slice: """ self.startStreams(self, [ @@ -1129,7 +1174,7 @@ class Api: """ # Get telemetry only of the links in the shortest path: - await self.startStreams([ f"{shortestPath[i]}-{shortestPath[i+1]}" for i in range(len(shortestPath)-1) ], sliceId) + await self.startStreams(shortestPathLinks, sliceId) return { "message": f"No telemetry available for slice '{sliceId}'" } else: return { "message": f"There is no slice with id '{sliceId}' registered" } -- GitLab From d8de7c76a2945049a418bd29a08397da24d23053 Mon Sep 17 00:00:00 2001 From: santoss Date: Wed, 27 May 2026 14:45:39 +0200 Subject: [PATCH 18/27] Add GAIA Architecture --- .DS_Store | Bin 0 -> 6148 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/api/main.py | 39 ++++++++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0387394593eb6a564c8dbd9de88228aceaa7c799 GIT binary patch literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLftjJ0p@<=mA&7utoqAWI#~@svK0^gR5djNE-sg2WcN|2*3hplpYO%0SEyAmZ=eR literal 0 HcmV?d00001 diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..45e350a5457c5e7e299d845881b71036b878945f GIT binary patch literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLfrY`H!I8m}!7(kRI5{alKL@Ie2}*<1 zFoMiLlZQ%=QllX-8UmvsFd71*Aut*OgD3<*^){&f=N?3E8gbS3Ltr!nhHD5gvbY4hIDsln96kiqxd~7? z5F1n#GlJ@5h#qiN3~K~1K?W2hpvpnjJ-8}ngtQ?*e316hh5#&pM(NQI7=RD}07=Rc A2><{9 literal 0 HcmV?d00001 diff --git a/src/api/main.py b/src/api/main.py index f051ebf..ed1e165 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -335,10 +335,14 @@ class Api: result = self.slice_service.nsc(intent) if result: create_data_store(intent) + + slos = safe_get(intent, ['ietf-network-slice-service:network-slice-services', 'slo-sle-templates', 'slo-sle-template'], []) + slosDict = { slo["id"]: { "slo-policy": slo["slo-policy"], "sle-policy": slo["sle-policy"] } for slo in slos } for slice in result["slices"]: current_app.config["slices"][slice["id"]] = { "src": slice["source"], - "dst": slice["destination"] + "dst": slice["destination"], + "slo-sle-policy": slosDict.get(slice.get("slo-sle-template", ""), {}), } logging.info(f"Network Slice created successfully") return send_response( @@ -867,10 +871,12 @@ class Api: logging.info(f"JSON detected: {data}") telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + bw = telemetry.get("bandwidth-utilization", "N/A") + latency = telemetry.get("latency", "N/A") self.telemetry_cache.setdefault(slice_id, {})[link_id] = { "timestamp": data["notification"]["eventTime"], - "bandwidth": float(telemetry.get("bandwidth-utilization", 0)), - "latency": float(telemetry.get("latency", 0)), + "bandwidth": float(bw) if bw != "N/A" else bw, + "latency": float(latency) if latency != "N/A" else latency, "services": telemetry.get("related-service-ids", []) } except json.JSONDecodeError: logging.error("Error parsing the JSON from field 'data'") @@ -1107,7 +1113,7 @@ class Api: ] if True else (await response.json()) #""" - network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == sliceId), None) + network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == "admin"), None) logging.info(f"Retrieved topology for slice '{sliceId}': {network}") return network, 200 @@ -1142,13 +1148,24 @@ class Api: "services": List[any] } """ + + slo = current_app.config["slices"][sliceId]["slo-sle-policy"].get("slo-policy", {}) + sloBandwith = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-bandwidth" ][0] if slo else 0 + sloLatency = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-delay-maximum" ][0] if slo else "N/A" + + bw = min([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()]) + lt = sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()]) + + warningJson = { "warning": {} } + if sloLatency != "N/A" and lt > float(sloLatency): warningJson["warning"]["latency"] = f"High latency: {lt}ms exceeds SLO of {sloLatency}ms" + if sloBandwith != 0 and bw > float(sloBandwith): warningJson["warning"]["bandwidth"] = f"High bandwidth utilization: {bw}Kb/s exceeds SLO of {sloBandwith}Kb/s" return { "sliceId": sliceId, "telemetry": { - "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields should be included in the real version.", - "latency": f"{sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()])}ms", - "bandwidth": f"{sum([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()])}Mb/s" - }, + "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields could be included in the future.", + "latency": f"{lt}ms", + "bandwidth": f"{bw}Mb/s" + } + warningJson, "timestamp": time.time() } else: @@ -1291,8 +1308,12 @@ class Api: while okToProceed: try: data, code = await self.getTelemetrySubscriptions(clientId, sliceId, True) + if code == 200: - yield f"data: {json.dumps(data)}\n\n" + if "warning" in data["sliceSubscription"]["telemetry"]: + logging.warning(f"Warning for client '{clientId}' in slice '{sliceId}': {data['sliceSubscription']['telemetry']['warning']}") + yield f"event: warning\ndata: {json.dumps(data)}\n\n" + else: yield f"data: {json.dumps(data)}\n\n" await asyncio.sleep(frequency) else: yield f"event: error\ndata: {json.dumps(data)}\n\n" -- GitLab From 837910bc4d975bc20e0978a861e8da8f5322491b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Tue, 5 May 2026 10:50:58 +0000 Subject: [PATCH 19/27] Update copyright headers --- out-no-header.txt | 16 ++ out-skipped.txt | 381 ++++++++++++++++++++++++++++++++++++++++ out-updated.txt | 88 ++++++++++ src/config/.env.example | 2 +- 4 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 out-no-header.txt create mode 100644 out-skipped.txt create mode 100644 out-updated.txt diff --git a/out-no-header.txt b/out-no-header.txt new file mode 100644 index 0000000..03c635a --- /dev/null +++ b/out-no-header.txt @@ -0,0 +1,16 @@ +./slice.db +./yang-modules/ietf-ac-common@2025-09-29.yang +./yang-modules/ietf-key-chain@2017-06-15.yang +./yang-modules/ietf-routing-types@2017-12-04.yang +./yang-modules/ietf-network@2018-02-26.yang +./yang-modules/ietf-ethertypes@2019-03-04.yang +./yang-modules/ietf-netconf-acm@2018-02-14.yang +./yang-modules/ietf-vpn-common@2022-02-11.yang +./yang-modules/ietf-te-types@2025-10-17.yang +./yang-modules/ietf-geo-location@2022-02-11.yang +./yang-modules/ietf-ac-svc@2025-09-29.yang +./yang-modules/ietf-packet-fields@2019-03-04.yang +./yang-modules/ietf-te-packet-types@2025-01-24.yang +./yang-modules/tree.txt +./yang-modules/ietf-network-topology@2018-02-26.yang +./yang-modules/ietf-network-slice-service@2025-05-09.yang diff --git a/out-skipped.txt b/out-skipped.txt new file mode 100644 index 0000000..9a01149 --- /dev/null +++ b/out-skipped.txt @@ -0,0 +1,381 @@ +./.env +./out-skipped.txt +./README.md +./LICENSE +./out-updated.txt +./out-no-header.txt +./images/NSC_Architecture.png +./src/mapper/__pycache__/main.cpython-312.pyc +./src/mapper/__pycache__/slo_viability.cpython-312.pyc +./src/mapper/__pycache__/slo_viability.cpython-39.pyc +./src/mapper/__pycache__/main.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-39.pyc +./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-312.pyc +./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-312.pyc +./src/realizer/ixia/__pycache__/main.cpython-312.pyc +./src/realizer/ixia/__pycache__/ixia_connect.cpython-312.pyc +./src/realizer/ixia/__pycache__/ixia_connect.cpython-39.pyc +./src/realizer/ixia/__pycache__/main.cpython-39.pyc +./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-312.pyc +./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-312.pyc +./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-39.pyc +./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-39.pyc +./src/realizer/e2e/__pycache__/e2e_connect.cpython-39.pyc +./src/realizer/e2e/__pycache__/main.cpython-312.pyc +./src/realizer/e2e/__pycache__/e2e_connect.cpython-312.pyc +./src/realizer/e2e/__pycache__/main.cpython-39.pyc +./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-312.pyc +./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-39.pyc +./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-312.pyc +./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-39.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-312.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-39.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-312.pyc +./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-39.pyc +./src/realizer/tfs/__pycache__/main.cpython-312.pyc +./src/realizer/tfs/__pycache__/tfs_connect.cpython-39.pyc +./src/realizer/tfs/__pycache__/tfs_connect.cpython-312.pyc +./src/realizer/tfs/__pycache__/main.cpython-39.pyc +./src/realizer/__pycache__/send_controller.cpython-39.pyc +./src/realizer/__pycache__/main.cpython-312.pyc +./src/realizer/__pycache__/send_controller.cpython-312.pyc +./src/realizer/__pycache__/select_way.cpython-39.pyc +./src/realizer/__pycache__/select_way.cpython-312.pyc +./src/realizer/__pycache__/nrp_handler.cpython-312.pyc +./src/realizer/__pycache__/nrp_handler.cpython-39.pyc +./src/realizer/__pycache__/main.cpython-39.pyc +./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-312.pyc +./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-39.pyc +./src/planner/hrat_planner/__pycache__/hrat.cpython-312.pyc +./src/planner/hrat_planner/__pycache__/hrat.cpython-39.pyc +./src/planner/__pycache__/planner.cpython-39.pyc +./src/planner/__pycache__/planner.cpython-312.pyc +./src/planner/energy_planner/topo_ddbb.json +./src/planner/energy_planner/energy_ddbb.json +./src/planner/energy_planner/ext_topo_ddbb.json +./src/planner/energy_planner/__pycache__/energy.cpython-39.pyc +./src/planner/energy_planner/__pycache__/energy.cpython-312.pyc +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_1.json +./src/tests/requests/3gpp_template_UC1PoC2_midhaul.json +./src/tests/requests/l3vpn_test.json +./src/tests/requests/ietf_green_request.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul.json +./src/tests/requests/frr_request.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_2.json +./src/tests/requests/create_slice_1.json +./src/tests/requests/ietf_template_timing.json +./src/tests/requests/slice_request.json +./src/tests/requests/P2MP.json +./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_3.json +./src/tests/requests/3ggpp_template_green.json +./src/config/IPs.json +./src/config/__pycache__/config.cpython-39.pyc +./src/config/__pycache__/constants.cpython-39.pyc +./src/config/__pycache__/config.cpython-312.pyc +./src/config/__pycache__/constants.cpython-312.pyc +./src/database/nrp_ddbb.json +./src/database/__pycache__/db.cpython-312.pyc +./src/database/__pycache__/sysrepo_store.cpython-39.pyc +./src/database/__pycache__/store_data.cpython-312.pyc +./src/database/__pycache__/db.cpython-39.pyc +./src/database/__pycache__/store_data.cpython-39.pyc +./src/database/__pycache__/sysrepo_store.cpython-312.pyc +./src/templates/ietfL3VPN_template_empty.json +./src/templates/L3-VPN_template_empty.json +./src/templates/Optical_slice.json +./src/templates/3gpp_template_empty.json +./src/templates/ietf_template_empty.json +./src/templates/TAPI_service.json +./src/templates/L2-VPN_template_empty.json +./src/templates/ietfL2VPN_template_empty.json +./src/templates/IPoWDM_orchestrator.json +./src/nbi_processor/__pycache__/main.cpython-312.pyc +./src/nbi_processor/__pycache__/translator.cpython-312.pyc +./src/nbi_processor/__pycache__/translator.cpython-39.pyc +./src/nbi_processor/__pycache__/detect_format.cpython-312.pyc +./src/nbi_processor/__pycache__/main.cpython-39.pyc +./src/nbi_processor/__pycache__/detect_format.cpython-39.pyc +./src/api/__pycache__/main.cpython-312.pyc +./src/api/__pycache__/main.cpython-39.pyc +./src/__pycache__/main.cpython-312.pyc +./src/__pycache__/main.cpython-39.pyc +./src/webui/static/img/MiniLogo.png +./src/webui/static/img/TelefonicaLogo.png +./src/webui/static/img/banner.png +./src/webui/__pycache__/gui.cpython-39.pyc +./src/webui/__pycache__/gui.cpython-312.pyc +./src/utils/__pycache__/dump_templates.cpython-39.pyc +./src/utils/__pycache__/safe_get.cpython-39.pyc +./src/utils/__pycache__/safe_get.cpython-312.pyc +./src/utils/__pycache__/build_response.cpython-312.pyc +./src/utils/__pycache__/load_template.cpython-312.pyc +./src/utils/__pycache__/send_response.cpython-312.pyc +./src/utils/__pycache__/dump_templates.cpython-312.pyc +./src/utils/__pycache__/build_response.cpython-39.pyc +./src/utils/__pycache__/load_template.cpython-39.pyc +./src/utils/__pycache__/send_response.cpython-39.pyc +./swagger/restconf_swagger_full.json +./swagger/restconf_swagger.json +./swagger/models/__pycache__/create_models.cpython-39.pyc +./swagger/models/__pycache__/create_models.cpython-312.pyc +./swagger/models/__pycache__/create_models_restconf.cpython-312.pyc +./swagger/__pycache__/E2E_namespace.cpython-39.pyc +./swagger/__pycache__/ixia_namespace.cpython-312.pyc +./swagger/__pycache__/tfs_namespace.cpython-312.pyc +./swagger/__pycache__/restconf_namespace.cpython-312.pyc +./swagger/__pycache__/tfs_namespace.cpython-39.pyc +./swagger/__pycache__/E2E_namespace.cpython-312.pyc +./swagger/__pycache__/ixia_namespace.cpython-39.pyc +./.gitlab/issue_templates/enhancement.md +./.gitlab/issue_templates/new-feature.md +./.gitlab/issue_templates/bug.md +./.gitlab/issue_templates/new-feature-with-design.md +./.git/index +./.git/HEAD +./.git/ORIG_HEAD +./.git/FETCH_HEAD +./.git/description +./.git/config +./.git/packed-refs +./.git/COMMIT_EDITMSG +./.git/objects/a1/c55879cb8c020c875f9237f726c4cf110297aa +./.git/objects/24/319c155fb104e20bee79e5b257317b01323197 +./.git/objects/d3/a1c91438954552d86907f8e2ab41a0cdf0c8e4 +./.git/objects/d9/da81eeebefad9054a43af552de3d51f20bcd56 +./.git/objects/bc/0bbade1bca1444abc8e4f834a7d0e3ad714bcc +./.git/objects/83/b8d3d4ad4e28b52e88d38a769f56109b5b8e54 +./.git/objects/13/364ab58163d44a3e6bbfbf25d7cb3b0f484e98 +./.git/objects/13/0ef01e5b6b9388458379a3602b1ec98374bc31 +./.git/objects/84/ffcc16d441fee227dd11117d87e2945cbb85d7 +./.git/objects/56/94d78e71d85b1da4eff3ad3a45af3acf808d9f +./.git/objects/56/930c81de3639098c8c42a511fafdb154c7b26e +./.git/objects/56/a52ec183ae5731cd400b47f3892bc3cdd93f6b +./.git/objects/ce/5604665f56b58b25593deeaf3c38db07db9ef3 +./.git/objects/c7/2736f92d46a5befe01e19a7a8d3049966952c8 +./.git/objects/68/4d191ecb38ed786b6be8faa19b58b1fc684c9b +./.git/objects/9a/50709c0681b663042d652f067c4929e7d61d4c +./.git/objects/9a/1e5b2c267e9226e3af34f8b2047bb9b38018ad +./.git/objects/b2/015d83dadd94e23940447d942d1d8ea6398ad3 +./.git/objects/2a/a4667a5e3db8e6de820394831a54d36fae96ed +./.git/objects/2a/62635019070c7a6edb0988ddc3216f844336ac +./.git/objects/2a/369c6bd4be9badb160d4bdee49cfb5ea605e0b +./.git/objects/30/623e9b710b9f32a386a0c1ff693d322a8cba88 +./.git/objects/ba/cdaf84f155b43df2b89ef0e4efc3d57b0d7fff +./.git/objects/0c/6e465d74277bed20db3cbbb8d6bc9e9c041e65 +./.git/objects/29/5a0d32051bb651cc0dcdd6fefae0468094a7c6 +./.git/objects/1e/c944d791db1da1b8236c6069f10d65b1b6f97f +./.git/objects/1e/6bf8411348d038dc8ab93114c0ec787f2fac1f +./.git/objects/1e/8cf0817554ec18499a53d9c401a3f54d6d49e5 +./.git/objects/c9/ae55eb708854fd67b73410a4f8715e64b3aa57 +./.git/objects/6c/ea7d3642dab6ba8ac09f3e44a07d6591ba99f4 +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.idx +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.rev +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.idx +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.pack +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.idx +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.pack +./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.rev +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.pack +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.idx +./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.rev +./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.pack +./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.rev +./.git/objects/a9/1024508763398543610cea7b8e16fa952d28b7 +./.git/objects/a9/cd1d4586eb8befff4a7e90df781eb7806aed39 +./.git/objects/f8/0e69f3bdf2ec564923cfaba9b88ac70882b77c +./.git/objects/f8/00d42dbcc2f59c613d1007c4738e2acb0355d2 +./.git/objects/8f/37e643276bf994d955e3dad95a4cb93c258f67 +./.git/objects/8f/132c895e2946db20e93bd54149330ac0226bfb +./.git/objects/17/5b00798abd846f9413b56e1d3de4937f832684 +./.git/objects/17/eb5d4435e7efcc94acf8e32506c0705cedb8e5 +./.git/objects/80/0c62fe8e5147086dd9f07d03aabaf297a3287f +./.git/objects/e3/8daf12a35eda16070034191c7a2c5220c5e057 +./.git/objects/e3/019e59e794d66493817c9d76c75ba7718ec4df +./.git/objects/da/3ef7a52980a769f2ed10e1e1167995af16576c +./.git/objects/da/857ab54a12b2dbe6315042b7bdfccc1aff19cc +./.git/objects/50/b00fbe85eeaa64bcbe8159c3d04164d3ea9113 +./.git/objects/d1/f11cd63da20457265e6a159531c05b8b0ea75e +./.git/objects/82/b67a1ca7cf7b568cb9fa566a6ab065072aaa2e +./.git/objects/5e/79c73f4c3a8a1be7f0cb84f37d0cf93a9b6da0 +./.git/objects/5e/da6a83ccffc29200838e23f5b7feecbc3aa8ce +./.git/objects/5e/fe8d47ca5f0ec8c2f89d4dbbf91de604da0565 +./.git/objects/7f/24267ce03103755f28099cc124c0a821fd739e +./.git/objects/7f/6f0a8a616fc2eb18dbd69255ff35ef43041931 +./.git/objects/cf/da9cf8b25de8bc8dfbe63f728e5e929e6bd66b +./.git/objects/cf/f183b0d73d0a799af3a09f19583ecc6e780833 +./.git/objects/cf/aa9ee46f37057b0cba026a5d042d1ef3f6438d +./.git/objects/7c/c66d11d7ba06287896a33b2c018c2061e22851 +./.git/objects/87/28250df41f46adc8fa6b412e676c3e0117fa41 +./.git/objects/43/0c261a579e911ee8742490dbc87238a1c1cc08 +./.git/objects/4d/ebe53ee691012be9a33b44b6ee508315db8ca9 +./.git/objects/eb/56ca633390c63c058c351e3c9492b6ef5374f5 +./.git/objects/d2/46c96bd2ff97920f294fb6b0213cc7c70137c2 +./.git/objects/7b/ce196c1667ee6c8f708e674a7f8e20724ecfc0 +./.git/objects/65/d4040710f3628da6fe84d5a538c1675fb1ccb9 +./.git/objects/af/3bafe1661a00bb7cfc9f44157f34af84befa28 +./.git/objects/00/396bbf60369411bfb43b7a84a87f19facc5dee +./.git/objects/45/4eb176fe183a1a1eed5826548cc95f16fc18aa +./.git/objects/45/bb5358f6219afdd12de9058c0917fb8823b4a4 +./.git/objects/9c/e5fa89b675aa014166c81b58e73848aaf6042a +./.git/objects/ca/57941fc5a97914a1727ab42a988a8e8e3467ff +./.git/objects/52/a393230b2fb369d6954208a7504efb830442a9 +./.git/objects/52/d85bce8df704dc8890ddc79ecb6dcfd1295159 +./.git/objects/4b/4ae60160daa6c1e12ab2ad325e130ac62f557f +./.git/objects/8c/e41b9930a05732402ee3d8661f497297b6750f +./.git/objects/8c/ef7eeb08caf1349d4f1cdd3b52b8d01132e151 +./.git/objects/5a/aa083f84abd1c8ca2ffcb7d5c19bd63600611c +./.git/objects/a6/461e6335cfe0b97521db324595cb489fc1c41b +./.git/objects/2f/efbaab653094924acb3b0156c1eab15e752f85 +./.git/objects/2f/b797bd87bf4ed825f83ec788df707b94c5f68b +./.git/objects/e1/f13a86f6084d0906a384b0421385bb829d89d0 +./.git/objects/86/3e3ab7d7f0da019483b0293706569fdb295bc6 +./.git/objects/75/704a402d5653bdeb8b0bc90c011d67f1d0c3cf +./.git/objects/b7/dc48c63a8954330f2f8246e423dfb56a8563f5 +./.git/objects/41/baeecff513727319d7b0f395e822ecfd1fedc4 +./.git/objects/41/a7996ce90a56369c30484b143c931fd18b4518 +./.git/objects/c0/eca0991b29295a0afeaaa239d3b25aa2551d3d +./.git/objects/1a/2966234be8d0a24ffeead6dfb2525f645dcdbf +./.git/objects/1a/650c3076c03324bfdfe6754a125f0e1fa8a145 +./.git/objects/95/2b856ba4244babb1dab96d3698d0f286e073cd +./.git/objects/35/f862c574b53d55da5c4db62e29b305716b5d90 +./.git/objects/fd/055074aeba5c277bbefdce0b81ebd24d0d3551 +./.git/objects/96/5ddb39541fe96b8c71683b4ad5b63a08316daa +./.git/objects/96/418bb720ff04b7a36ee47b45a570692a7278a8 +./.git/objects/c2/9f6a40d317311ce82e756304033d5dd105c390 +./.git/objects/fc/9940922faff4ccccd2eaf705ed94f9982ac047 +./.git/objects/fc/16b40610986249153e18fc9650469671b46b95 +./.git/objects/90/ed7fa37077ddad67e2f82d7881df2f617be577 +./.git/objects/6f/3933c422c188054aeec25d640541348bde5fdd +./.git/objects/6f/7ec43b9fc24a5f9cc2de84422ad4f027fec93d +./.git/objects/60/7f11e8e85f7b241937efdf91a4f43b14e65ab9 +./.git/objects/25/67a613ebdf545a1f1bd650566ba947162efc35 +./.git/objects/36/83a2581ba7d2517a0b5e59edc594893ae6d99c +./.git/objects/36/f8d72d0627516b64420ed0bc1175700f711857 +./.git/objects/51/abd33613baeebbad2202efca9e022c6d55149f +./.git/objects/51/47a8e3ae3343937f77bc56f3b3048988e35404 +./.git/objects/0a/28bb8a469ecce2bacd0e3d57255964cad4e7ca +./.git/objects/2e/ba42828c5ff7ae54dd242beab47700b79b987d +./.git/objects/f5/fcc3572df93b2752b1ed393cb09e0c501f7a15 +./.git/objects/ef/16b4394fee65ab326bee56e80c5069a844992c +./.git/objects/67/be760a1c939c84bb1310a22ff1f35ea9c9abb0 +./.git/objects/8b/e0afbc8d5f2cadc93eb685a4f210aab0458de2 +./.git/objects/33/45a89c51c736f929344b68f897e560fb7798e0 +./.git/objects/33/5b7e601c09e5751a80f346c9aafb93d17101d2 +./.git/objects/72/05a1fdf91869d35795d35df9a44fcf0a365e3f +./.git/objects/c5/59d204968a27b32f5eb3235a91b07317749239 +./.git/objects/93/2d002794afabda8f14c595d1e9644943ca2757 +./.git/objects/44/5d1994a5ac57366078b198200a9e143d4ccda8 +./.git/objects/5f/d71bb7aba1ce2209373b626e99e8b7d6f4c03f +./.git/objects/0d/65d4ea9dbfe0e79f437e9efbd78c56ada25688 +./.git/objects/b9/3f0df8f29b3d8070ed2a9f54bbe31029fe66e1 +./.git/objects/b9/7cd799d77968fdc4cdfef22365468470cf0eb4 +./.git/objects/4f/fb116001eeded5674daef6abef65089208ed1a +./.git/objects/ff/b1e0654e644d404272145c9e82d40c70da3fcf +./.git/objects/fb/df8b0da5f9b93d54bb5f221c3bc97704675004 +./.git/objects/6b/680462719b3b632f1a37f89ce79005df57ef23 +./.git/objects/6b/ffc7308bc85dde17a094c88e4db4c055f72488 +./.git/objects/6b/1c053d37cecbcb581c74be40a7b0a76d450575 +./.git/objects/85/d23814e172addd8c22c0f8fa0bd2f2a62212f5 +./.git/objects/b6/098d3a5d03e998d50c9b87b06c3c1940ee7c4f +./.git/objects/bf/4855faf0508a152471f6c6c8f756581b8ebb96 +./.git/objects/bf/45cf47afbe1af29b37202ffa7aad670f724315 +./.git/objects/c8/2b2f7a810646753627819a01e54b2fbb0bf994 +./.git/objects/fa/64f14a857fee267529d204b1459599cdcd4c1c +./.git/objects/64/76ab8696fde6f30a60496b6432e722be4f7ecb +./.git/objects/64/bc3be12e5a19ba7b8d2feef676f87de847c7ee +./.git/objects/31/9ae2c46027c14596f9cbb2b7feac7638a53ac3 +./.git/objects/63/5ac650b9e5cb5102933f3b31acd333c5a80893 +./.git/objects/63/e6828bfe6daec53b28903951dba03be01d3030 +./.git/objects/79/eb1ce74e5351a6f490e0982cbc6e129b4aa7c8 +./.git/objects/5c/ca0f79b82611f512e6294a0f00596a234eb85c +./.git/objects/b3/16e45895dfd681cc04ee3301c329a74fdba090 +./.git/objects/1f/8147d384285cfe5ff18e96ebeff64aca142ab2 +./.git/objects/3b/4c4f812a8d80d92927085740fee2bafe5a602f +./.git/objects/23/16ac2bb4ceac9df62c1ff1e1a8a7bdcb01e5d7 +./.git/objects/9b/3926088eae0e151326f6f0015829521df010b4 +./.git/objects/27/8591987f33b59eb256926e6d9c96a0cdb40b92 +./.git/objects/d5/a0cd040e7aba6e44e5cb6c48b3b31de7fa3996 +./.git/objects/d5/61671aaa8c1913d335a6750e9fad59f6065448 +./.git/objects/d5/d06687674c7ac43f59839c7d16ffe001f7d0c7 +./.git/objects/d5/8b47cc8995eda0d0902699f1037849b620e723 +./.git/objects/d5/4fbe58d8f2468a576a9cec2bcaaa26801f6b3d +./.git/objects/d5/45ab9b4098ec8c1818861356b9fe334552af1a +./.git/objects/fe/ea2538a9529bd851c99b976099e226409080c3 +./.git/objects/fe/4849b2f3a88b6f3997f565ad7d9da97c402a58 +./.git/objects/fe/b44f5214740a873ac5eedbb942834d8916e07d +./.git/objects/55/3aa665e46a83e53e331b2435e4eacd6e696ba0 +./.git/objects/55/b6eb9fc430d168b02cbf5a231d93bb7632ab31 +./.git/objects/a0/e4208fa05edf37e2f64955495172d860048b74 +./.git/objects/7e/db6adeb6c45b5b3524c1c33d2c1abdfd2ef4b3 +./.git/objects/c4/507da960010d98b9560a34fe55686858f1116b +./.git/objects/38/a157dfa5222bb8aad0c3531a07c0eb288a9f0a +./.git/objects/b8/15446f819ac2dcba50e63e5781e8a1be9b59c6 +./.git/objects/74/d094765ec06d081a8eb0cf62a2e6f3812829a0 +./.git/objects/d4/7bcee05a434c6b87726710fcc854971a72d606 +./.git/objects/09/cad2ec21e916c54b3aebb07431df19760c0e25 +./.git/objects/a4/a10b59d02c8c64af41d0bb0255873e888ea89d +./.git/objects/e0/1a219d20f0a4e2a6fb4747565f072e89dbe3ee +./.git/objects/53/27cdf631d950ea1e10d0517bdcd67179e9721f +./.git/objects/37/30fb786f1b523cd179652a96f821ce88eea36d +./.git/objects/37/650c30d77f69b0b011040293e053a4e7c6f98c +./.git/objects/28/f953b838d0200c67301cc7f3a0d352e26f3c42 +./.git/objects/f2/d19117ef0cd800c52fde833c32acb71e46a664 +./.git/objects/4c/5dd0a4298e91212e5422fa71254fc0e5176158 +./.git/objects/4c/be96ff54343b2f28a2b5069343ec49a4342ba9 +./.git/objects/a8/d605fb18e5c5ee1f3f5352c1460e1f760535bd +./.git/objects/a8/d7d38e73f05bf78472f59a092bdef3949483e7 +./.git/objects/b0/165037a79df96cdc3570f14250ae79bb3e693e +./.git/objects/b0/af64a3d1d8dc88b08bb9208dea14f8761f973f +./.git/objects/2b/38518e1f2a3349a23737edb17748c0ec1428cc +./.git/objects/2b/52a895406f2999f2fed4bbb21dcd33d7569d6a +./.git/objects/b4/80245fb3f89a446b36d21c8b1b6df792e2e5a8 +./.git/objects/7a/6d78346ba3078860f945a4f56f88a8e50d2b42 +./.git/objects/58/f8841a3fc96b9e14be040842de44b85e5276cc +./.git/objects/b1/8e4237f7f9851fbf4180cd4d0549497c165637 +./.git/objects/b1/afe94b6b0a3bd565aabc05acd34e487b914071 +./.git/objects/c1/df659fb1b5539c087fa05da398c1ffa81712b8 +./.git/objects/c1/eb94b7d6938b2ec524f98d5b6ea9276139d26d +./.git/objects/c1/b79b5e4a0d723e1c8a437c21bba6846b39a9c8 +./.git/objects/81/3380a694d4f4961a9feea10d881c3554d41155 +./.git/objects/81/cedd6d7915b885fefd382950534d4d5ee4d2de +./.git/objects/e2/d340bf4d50c22789fcae8145de3d53e9487bc8 +./.git/objects/e2/2ce8b7aa5354f50987e9646e7b20274d7d8f3f +./.git/objects/e2/6091b8dfab7cd3efd7f0ce8f7b1dc8bd540f6f +./.git/objects/9e/73b99fe785e2c18588575ebea9e63409e6390b +./.git/objects/34/3455561e55393181b9ad342eedb62b242a59ca +./.git/objects/2d/9823652e7ca859d5f826b475c4d7a9bc979919 +./.git/logs/HEAD +./.git/logs/refs/heads/main +./.git/logs/refs/heads/develop +./.git/logs/refs/heads/feat/25-tid-improve-mapping-of-slice-requests +./.git/logs/refs/heads/feat/24-tid-restconf-base-nbi +./.git/logs/refs/remotes/origin/sat_module +./.git/logs/refs/remotes/origin/HEAD +./.git/logs/refs/remotes/origin/develop +./.git/logs/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests +./.git/logs/refs/remotes/origin/feat/24-tid-restconf-base-nbi +./.git/refs/heads/main +./.git/refs/heads/develop +./.git/refs/heads/feat/25-tid-improve-mapping-of-slice-requests +./.git/refs/heads/feat/24-tid-restconf-base-nbi +./.git/refs/remotes/origin/sat_module +./.git/refs/remotes/origin/HEAD +./.git/refs/remotes/origin/develop +./.git/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests +./.git/refs/remotes/origin/feat/24-tid-restconf-base-nbi +./.git/info/exclude +./.git/hooks/post-update.sample +./.git/hooks/pre-push.sample +./.git/hooks/sendemail-validate.sample +./.git/hooks/pre-receive.sample +./.git/hooks/pre-merge-commit.sample +./.git/hooks/pre-rebase.sample +./.git/hooks/update.sample +./.git/hooks/pre-applypatch.sample +./.git/hooks/fsmonitor-watchman.sample +./.git/hooks/push-to-checkout.sample +./.git/hooks/applypatch-msg.sample +./.git/hooks/pre-commit.sample +./.git/hooks/commit-msg.sample +./.git/hooks/prepare-commit-msg.sample diff --git a/out-updated.txt b/out-updated.txt new file mode 100644 index 0000000..6953feb --- /dev/null +++ b/out-updated.txt @@ -0,0 +1,88 @@ +./Dockerfile +./.gitignore +./app.py +./deploy.sh +./.dockerignore +./.gitlab-ci.yml +./requirements.txt +./src/main.py +./src/mapper/process_connnectivity.py +./src/mapper/main.py +./src/mapper/get_service_template.py +./src/mapper/get_template.py +./src/mapper/slo_viability.py +./src/mapper/extract_sdp_info.py +./src/realizer/nrp_handler.py +./src/realizer/main.py +./src/realizer/send_controller.py +./src/realizer/select_way.py +./src/realizer/ixia/ixia_connect.py +./src/realizer/ixia/main.py +./src/realizer/ixia/helpers/automatizacion_ne2v4.py +./src/realizer/ixia/helpers/NEII_V4.py +./src/realizer/restconf/restconf_connect.py +./src/realizer/restconf/main.py +./src/realizer/restconf/connectors/tfs_connector.py +./src/realizer/restconf/connectors/frr_connector.py +./src/realizer/restconf/connectors/cisco_connector.py +./src/realizer/restconf/service_types/l3vpn.py +./src/realizer/restconf/service_types/l2vpn.py +./src/realizer/restconf/service_types/builders/configure_match_criteria.py +./src/realizer/restconf/service_types/builders/create_network_access.py +./src/realizer/restconf/service_types/builders/configure_slos.py +./src/realizer/restconf/service_types/builders/create_site_from_sdp.py +./src/realizer/restconf/service_types/builders/initialize_structure.py +./src/realizer/restconf/service_types/builders/apply_metric_constraint.py +./src/realizer/e2e/e2e_connect.py +./src/realizer/e2e/main.py +./src/realizer/e2e/service_types/del_l3ipowdm_slice.py +./src/realizer/e2e/service_types/l3ipowdm_slice.py +./src/realizer/tfs/tfs_connect.py +./src/realizer/tfs/main.py +./src/realizer/tfs/helpers/tfs_connector.py +./src/realizer/tfs/helpers/cisco_connector.py +./src/realizer/tfs/service_types/tfs_l2vpn.py +./src/realizer/tfs/service_types/tfs_l3vpn.py +./src/planner/planner.py +./src/planner/tfs_optical_planner/tfs_optical.py +./src/planner/hrat_planner/hrat.py +./src/planner/energy_planner/energy.py +./src/tests/test_e2e.py +./src/tests/test_mapper.py +./src/tests/test_api.py +./src/tests/test_utils.py +./src/tests/test_nbi_processor.py +./src/tests/test_database.py +./src/tests/test_initialization.py +./src/config/config.py +./src/config/.env.example +./src/config/constants.py +./src/database/store_data.py +./src/database/db.py +./src/database/sysrepo_store.py +./src/nbi_processor/detect_format.py +./src/nbi_processor/main.py +./src/nbi_processor/translator.py +./src/api/main.py +./src/webui/gui.py +./src/webui/templates/dev.html +./src/webui/templates/search.html +./src/webui/templates/login.html +./src/webui/templates/index.html +./src/webui/templates/welcome.html +./src/webui/templates/ixia.html +./src/utils/build_response.py +./src/utils/send_response.py +./src/utils/load_template.py +./src/utils/safe_get.py +./src/utils/dump_templates.py +./src/utils/slice_manager.py +./swagger/tfs_namespace.py +./swagger/ixia_namespace.py +./swagger/restconf_namespace.py +./swagger/E2E_namespace.py +./swagger/models/create_models_restconf.py +./swagger/models/create_models.py +./scripts/update_license_headers.py +./scripts/dump_templates_nsc.sh +./scripts/show_logs_nsc.sh diff --git a/src/config/.env.example b/src/config/.env.example index 1e45691..977a874 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -66,7 +66,7 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=127.0.0.1 +RESTCONF_IP=192.168.27.165 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO -- GitLab From c40920fa7dcc7834e8d6d65687605adc22e8d66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Wed, 6 May 2026 07:45:35 +0000 Subject: [PATCH 20/27] Delete copyright headers script log files --- out-no-header.txt | 16 -- out-skipped.txt | 381 ---------------------------------------------- out-updated.txt | 88 ----------- 3 files changed, 485 deletions(-) delete mode 100644 out-no-header.txt delete mode 100644 out-skipped.txt delete mode 100644 out-updated.txt diff --git a/out-no-header.txt b/out-no-header.txt deleted file mode 100644 index 03c635a..0000000 --- a/out-no-header.txt +++ /dev/null @@ -1,16 +0,0 @@ -./slice.db -./yang-modules/ietf-ac-common@2025-09-29.yang -./yang-modules/ietf-key-chain@2017-06-15.yang -./yang-modules/ietf-routing-types@2017-12-04.yang -./yang-modules/ietf-network@2018-02-26.yang -./yang-modules/ietf-ethertypes@2019-03-04.yang -./yang-modules/ietf-netconf-acm@2018-02-14.yang -./yang-modules/ietf-vpn-common@2022-02-11.yang -./yang-modules/ietf-te-types@2025-10-17.yang -./yang-modules/ietf-geo-location@2022-02-11.yang -./yang-modules/ietf-ac-svc@2025-09-29.yang -./yang-modules/ietf-packet-fields@2019-03-04.yang -./yang-modules/ietf-te-packet-types@2025-01-24.yang -./yang-modules/tree.txt -./yang-modules/ietf-network-topology@2018-02-26.yang -./yang-modules/ietf-network-slice-service@2025-05-09.yang diff --git a/out-skipped.txt b/out-skipped.txt deleted file mode 100644 index 9a01149..0000000 --- a/out-skipped.txt +++ /dev/null @@ -1,381 +0,0 @@ -./.env -./out-skipped.txt -./README.md -./LICENSE -./out-updated.txt -./out-no-header.txt -./images/NSC_Architecture.png -./src/mapper/__pycache__/main.cpython-312.pyc -./src/mapper/__pycache__/slo_viability.cpython-312.pyc -./src/mapper/__pycache__/slo_viability.cpython-39.pyc -./src/mapper/__pycache__/main.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-39.pyc -./src/realizer/ixia/helpers/__pycache__/NEII_V4.cpython-312.pyc -./src/realizer/ixia/helpers/__pycache__/automatizacion_ne2v4.cpython-312.pyc -./src/realizer/ixia/__pycache__/main.cpython-312.pyc -./src/realizer/ixia/__pycache__/ixia_connect.cpython-312.pyc -./src/realizer/ixia/__pycache__/ixia_connect.cpython-39.pyc -./src/realizer/ixia/__pycache__/main.cpython-39.pyc -./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-312.pyc -./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-312.pyc -./src/realizer/e2e/service_types/__pycache__/del_l3ipowdm_slice.cpython-39.pyc -./src/realizer/e2e/service_types/__pycache__/l3ipowdm_slice.cpython-39.pyc -./src/realizer/e2e/__pycache__/e2e_connect.cpython-39.pyc -./src/realizer/e2e/__pycache__/main.cpython-312.pyc -./src/realizer/e2e/__pycache__/e2e_connect.cpython-312.pyc -./src/realizer/e2e/__pycache__/main.cpython-39.pyc -./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-312.pyc -./src/realizer/tfs/helpers/__pycache__/cisco_connector.cpython-39.pyc -./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-312.pyc -./src/realizer/tfs/helpers/__pycache__/tfs_connector.cpython-39.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-312.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l2vpn.cpython-39.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-312.pyc -./src/realizer/tfs/service_types/__pycache__/tfs_l3vpn.cpython-39.pyc -./src/realizer/tfs/__pycache__/main.cpython-312.pyc -./src/realizer/tfs/__pycache__/tfs_connect.cpython-39.pyc -./src/realizer/tfs/__pycache__/tfs_connect.cpython-312.pyc -./src/realizer/tfs/__pycache__/main.cpython-39.pyc -./src/realizer/__pycache__/send_controller.cpython-39.pyc -./src/realizer/__pycache__/main.cpython-312.pyc -./src/realizer/__pycache__/send_controller.cpython-312.pyc -./src/realizer/__pycache__/select_way.cpython-39.pyc -./src/realizer/__pycache__/select_way.cpython-312.pyc -./src/realizer/__pycache__/nrp_handler.cpython-312.pyc -./src/realizer/__pycache__/nrp_handler.cpython-39.pyc -./src/realizer/__pycache__/main.cpython-39.pyc -./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-312.pyc -./src/planner/tfs_optical_planner/__pycache__/tfs_optical.cpython-39.pyc -./src/planner/hrat_planner/__pycache__/hrat.cpython-312.pyc -./src/planner/hrat_planner/__pycache__/hrat.cpython-39.pyc -./src/planner/__pycache__/planner.cpython-39.pyc -./src/planner/__pycache__/planner.cpython-312.pyc -./src/planner/energy_planner/topo_ddbb.json -./src/planner/energy_planner/energy_ddbb.json -./src/planner/energy_planner/ext_topo_ddbb.json -./src/planner/energy_planner/__pycache__/energy.cpython-39.pyc -./src/planner/energy_planner/__pycache__/energy.cpython-312.pyc -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_1.json -./src/tests/requests/3gpp_template_UC1PoC2_midhaul.json -./src/tests/requests/l3vpn_test.json -./src/tests/requests/ietf_green_request.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul.json -./src/tests/requests/frr_request.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_2.json -./src/tests/requests/create_slice_1.json -./src/tests/requests/ietf_template_timing.json -./src/tests/requests/slice_request.json -./src/tests/requests/P2MP.json -./src/tests/requests/3gpp_template_UC1PoC2_backhaul_request_3.json -./src/tests/requests/3ggpp_template_green.json -./src/config/IPs.json -./src/config/__pycache__/config.cpython-39.pyc -./src/config/__pycache__/constants.cpython-39.pyc -./src/config/__pycache__/config.cpython-312.pyc -./src/config/__pycache__/constants.cpython-312.pyc -./src/database/nrp_ddbb.json -./src/database/__pycache__/db.cpython-312.pyc -./src/database/__pycache__/sysrepo_store.cpython-39.pyc -./src/database/__pycache__/store_data.cpython-312.pyc -./src/database/__pycache__/db.cpython-39.pyc -./src/database/__pycache__/store_data.cpython-39.pyc -./src/database/__pycache__/sysrepo_store.cpython-312.pyc -./src/templates/ietfL3VPN_template_empty.json -./src/templates/L3-VPN_template_empty.json -./src/templates/Optical_slice.json -./src/templates/3gpp_template_empty.json -./src/templates/ietf_template_empty.json -./src/templates/TAPI_service.json -./src/templates/L2-VPN_template_empty.json -./src/templates/ietfL2VPN_template_empty.json -./src/templates/IPoWDM_orchestrator.json -./src/nbi_processor/__pycache__/main.cpython-312.pyc -./src/nbi_processor/__pycache__/translator.cpython-312.pyc -./src/nbi_processor/__pycache__/translator.cpython-39.pyc -./src/nbi_processor/__pycache__/detect_format.cpython-312.pyc -./src/nbi_processor/__pycache__/main.cpython-39.pyc -./src/nbi_processor/__pycache__/detect_format.cpython-39.pyc -./src/api/__pycache__/main.cpython-312.pyc -./src/api/__pycache__/main.cpython-39.pyc -./src/__pycache__/main.cpython-312.pyc -./src/__pycache__/main.cpython-39.pyc -./src/webui/static/img/MiniLogo.png -./src/webui/static/img/TelefonicaLogo.png -./src/webui/static/img/banner.png -./src/webui/__pycache__/gui.cpython-39.pyc -./src/webui/__pycache__/gui.cpython-312.pyc -./src/utils/__pycache__/dump_templates.cpython-39.pyc -./src/utils/__pycache__/safe_get.cpython-39.pyc -./src/utils/__pycache__/safe_get.cpython-312.pyc -./src/utils/__pycache__/build_response.cpython-312.pyc -./src/utils/__pycache__/load_template.cpython-312.pyc -./src/utils/__pycache__/send_response.cpython-312.pyc -./src/utils/__pycache__/dump_templates.cpython-312.pyc -./src/utils/__pycache__/build_response.cpython-39.pyc -./src/utils/__pycache__/load_template.cpython-39.pyc -./src/utils/__pycache__/send_response.cpython-39.pyc -./swagger/restconf_swagger_full.json -./swagger/restconf_swagger.json -./swagger/models/__pycache__/create_models.cpython-39.pyc -./swagger/models/__pycache__/create_models.cpython-312.pyc -./swagger/models/__pycache__/create_models_restconf.cpython-312.pyc -./swagger/__pycache__/E2E_namespace.cpython-39.pyc -./swagger/__pycache__/ixia_namespace.cpython-312.pyc -./swagger/__pycache__/tfs_namespace.cpython-312.pyc -./swagger/__pycache__/restconf_namespace.cpython-312.pyc -./swagger/__pycache__/tfs_namespace.cpython-39.pyc -./swagger/__pycache__/E2E_namespace.cpython-312.pyc -./swagger/__pycache__/ixia_namespace.cpython-39.pyc -./.gitlab/issue_templates/enhancement.md -./.gitlab/issue_templates/new-feature.md -./.gitlab/issue_templates/bug.md -./.gitlab/issue_templates/new-feature-with-design.md -./.git/index -./.git/HEAD -./.git/ORIG_HEAD -./.git/FETCH_HEAD -./.git/description -./.git/config -./.git/packed-refs -./.git/COMMIT_EDITMSG -./.git/objects/a1/c55879cb8c020c875f9237f726c4cf110297aa -./.git/objects/24/319c155fb104e20bee79e5b257317b01323197 -./.git/objects/d3/a1c91438954552d86907f8e2ab41a0cdf0c8e4 -./.git/objects/d9/da81eeebefad9054a43af552de3d51f20bcd56 -./.git/objects/bc/0bbade1bca1444abc8e4f834a7d0e3ad714bcc -./.git/objects/83/b8d3d4ad4e28b52e88d38a769f56109b5b8e54 -./.git/objects/13/364ab58163d44a3e6bbfbf25d7cb3b0f484e98 -./.git/objects/13/0ef01e5b6b9388458379a3602b1ec98374bc31 -./.git/objects/84/ffcc16d441fee227dd11117d87e2945cbb85d7 -./.git/objects/56/94d78e71d85b1da4eff3ad3a45af3acf808d9f -./.git/objects/56/930c81de3639098c8c42a511fafdb154c7b26e -./.git/objects/56/a52ec183ae5731cd400b47f3892bc3cdd93f6b -./.git/objects/ce/5604665f56b58b25593deeaf3c38db07db9ef3 -./.git/objects/c7/2736f92d46a5befe01e19a7a8d3049966952c8 -./.git/objects/68/4d191ecb38ed786b6be8faa19b58b1fc684c9b -./.git/objects/9a/50709c0681b663042d652f067c4929e7d61d4c -./.git/objects/9a/1e5b2c267e9226e3af34f8b2047bb9b38018ad -./.git/objects/b2/015d83dadd94e23940447d942d1d8ea6398ad3 -./.git/objects/2a/a4667a5e3db8e6de820394831a54d36fae96ed -./.git/objects/2a/62635019070c7a6edb0988ddc3216f844336ac -./.git/objects/2a/369c6bd4be9badb160d4bdee49cfb5ea605e0b -./.git/objects/30/623e9b710b9f32a386a0c1ff693d322a8cba88 -./.git/objects/ba/cdaf84f155b43df2b89ef0e4efc3d57b0d7fff -./.git/objects/0c/6e465d74277bed20db3cbbb8d6bc9e9c041e65 -./.git/objects/29/5a0d32051bb651cc0dcdd6fefae0468094a7c6 -./.git/objects/1e/c944d791db1da1b8236c6069f10d65b1b6f97f -./.git/objects/1e/6bf8411348d038dc8ab93114c0ec787f2fac1f -./.git/objects/1e/8cf0817554ec18499a53d9c401a3f54d6d49e5 -./.git/objects/c9/ae55eb708854fd67b73410a4f8715e64b3aa57 -./.git/objects/6c/ea7d3642dab6ba8ac09f3e44a07d6591ba99f4 -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.idx -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.rev -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.idx -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.pack -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.idx -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.pack -./.git/objects/pack/pack-108aab7fce6940a991c013355cab2a3f968c2a26.rev -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.pack -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.idx -./.git/objects/pack/pack-3d3fa8c0c3c689ae7de32b72c2eecc8e9fa35097.rev -./.git/objects/pack/pack-990e6aabe9f1db15ae5289db745a5fee90d4a840.pack -./.git/objects/pack/pack-5238d36cc15d3e031a7219cad2861fbef96222df.rev -./.git/objects/a9/1024508763398543610cea7b8e16fa952d28b7 -./.git/objects/a9/cd1d4586eb8befff4a7e90df781eb7806aed39 -./.git/objects/f8/0e69f3bdf2ec564923cfaba9b88ac70882b77c -./.git/objects/f8/00d42dbcc2f59c613d1007c4738e2acb0355d2 -./.git/objects/8f/37e643276bf994d955e3dad95a4cb93c258f67 -./.git/objects/8f/132c895e2946db20e93bd54149330ac0226bfb -./.git/objects/17/5b00798abd846f9413b56e1d3de4937f832684 -./.git/objects/17/eb5d4435e7efcc94acf8e32506c0705cedb8e5 -./.git/objects/80/0c62fe8e5147086dd9f07d03aabaf297a3287f -./.git/objects/e3/8daf12a35eda16070034191c7a2c5220c5e057 -./.git/objects/e3/019e59e794d66493817c9d76c75ba7718ec4df -./.git/objects/da/3ef7a52980a769f2ed10e1e1167995af16576c -./.git/objects/da/857ab54a12b2dbe6315042b7bdfccc1aff19cc -./.git/objects/50/b00fbe85eeaa64bcbe8159c3d04164d3ea9113 -./.git/objects/d1/f11cd63da20457265e6a159531c05b8b0ea75e -./.git/objects/82/b67a1ca7cf7b568cb9fa566a6ab065072aaa2e -./.git/objects/5e/79c73f4c3a8a1be7f0cb84f37d0cf93a9b6da0 -./.git/objects/5e/da6a83ccffc29200838e23f5b7feecbc3aa8ce -./.git/objects/5e/fe8d47ca5f0ec8c2f89d4dbbf91de604da0565 -./.git/objects/7f/24267ce03103755f28099cc124c0a821fd739e -./.git/objects/7f/6f0a8a616fc2eb18dbd69255ff35ef43041931 -./.git/objects/cf/da9cf8b25de8bc8dfbe63f728e5e929e6bd66b -./.git/objects/cf/f183b0d73d0a799af3a09f19583ecc6e780833 -./.git/objects/cf/aa9ee46f37057b0cba026a5d042d1ef3f6438d -./.git/objects/7c/c66d11d7ba06287896a33b2c018c2061e22851 -./.git/objects/87/28250df41f46adc8fa6b412e676c3e0117fa41 -./.git/objects/43/0c261a579e911ee8742490dbc87238a1c1cc08 -./.git/objects/4d/ebe53ee691012be9a33b44b6ee508315db8ca9 -./.git/objects/eb/56ca633390c63c058c351e3c9492b6ef5374f5 -./.git/objects/d2/46c96bd2ff97920f294fb6b0213cc7c70137c2 -./.git/objects/7b/ce196c1667ee6c8f708e674a7f8e20724ecfc0 -./.git/objects/65/d4040710f3628da6fe84d5a538c1675fb1ccb9 -./.git/objects/af/3bafe1661a00bb7cfc9f44157f34af84befa28 -./.git/objects/00/396bbf60369411bfb43b7a84a87f19facc5dee -./.git/objects/45/4eb176fe183a1a1eed5826548cc95f16fc18aa -./.git/objects/45/bb5358f6219afdd12de9058c0917fb8823b4a4 -./.git/objects/9c/e5fa89b675aa014166c81b58e73848aaf6042a -./.git/objects/ca/57941fc5a97914a1727ab42a988a8e8e3467ff -./.git/objects/52/a393230b2fb369d6954208a7504efb830442a9 -./.git/objects/52/d85bce8df704dc8890ddc79ecb6dcfd1295159 -./.git/objects/4b/4ae60160daa6c1e12ab2ad325e130ac62f557f -./.git/objects/8c/e41b9930a05732402ee3d8661f497297b6750f -./.git/objects/8c/ef7eeb08caf1349d4f1cdd3b52b8d01132e151 -./.git/objects/5a/aa083f84abd1c8ca2ffcb7d5c19bd63600611c -./.git/objects/a6/461e6335cfe0b97521db324595cb489fc1c41b -./.git/objects/2f/efbaab653094924acb3b0156c1eab15e752f85 -./.git/objects/2f/b797bd87bf4ed825f83ec788df707b94c5f68b -./.git/objects/e1/f13a86f6084d0906a384b0421385bb829d89d0 -./.git/objects/86/3e3ab7d7f0da019483b0293706569fdb295bc6 -./.git/objects/75/704a402d5653bdeb8b0bc90c011d67f1d0c3cf -./.git/objects/b7/dc48c63a8954330f2f8246e423dfb56a8563f5 -./.git/objects/41/baeecff513727319d7b0f395e822ecfd1fedc4 -./.git/objects/41/a7996ce90a56369c30484b143c931fd18b4518 -./.git/objects/c0/eca0991b29295a0afeaaa239d3b25aa2551d3d -./.git/objects/1a/2966234be8d0a24ffeead6dfb2525f645dcdbf -./.git/objects/1a/650c3076c03324bfdfe6754a125f0e1fa8a145 -./.git/objects/95/2b856ba4244babb1dab96d3698d0f286e073cd -./.git/objects/35/f862c574b53d55da5c4db62e29b305716b5d90 -./.git/objects/fd/055074aeba5c277bbefdce0b81ebd24d0d3551 -./.git/objects/96/5ddb39541fe96b8c71683b4ad5b63a08316daa -./.git/objects/96/418bb720ff04b7a36ee47b45a570692a7278a8 -./.git/objects/c2/9f6a40d317311ce82e756304033d5dd105c390 -./.git/objects/fc/9940922faff4ccccd2eaf705ed94f9982ac047 -./.git/objects/fc/16b40610986249153e18fc9650469671b46b95 -./.git/objects/90/ed7fa37077ddad67e2f82d7881df2f617be577 -./.git/objects/6f/3933c422c188054aeec25d640541348bde5fdd -./.git/objects/6f/7ec43b9fc24a5f9cc2de84422ad4f027fec93d -./.git/objects/60/7f11e8e85f7b241937efdf91a4f43b14e65ab9 -./.git/objects/25/67a613ebdf545a1f1bd650566ba947162efc35 -./.git/objects/36/83a2581ba7d2517a0b5e59edc594893ae6d99c -./.git/objects/36/f8d72d0627516b64420ed0bc1175700f711857 -./.git/objects/51/abd33613baeebbad2202efca9e022c6d55149f -./.git/objects/51/47a8e3ae3343937f77bc56f3b3048988e35404 -./.git/objects/0a/28bb8a469ecce2bacd0e3d57255964cad4e7ca -./.git/objects/2e/ba42828c5ff7ae54dd242beab47700b79b987d -./.git/objects/f5/fcc3572df93b2752b1ed393cb09e0c501f7a15 -./.git/objects/ef/16b4394fee65ab326bee56e80c5069a844992c -./.git/objects/67/be760a1c939c84bb1310a22ff1f35ea9c9abb0 -./.git/objects/8b/e0afbc8d5f2cadc93eb685a4f210aab0458de2 -./.git/objects/33/45a89c51c736f929344b68f897e560fb7798e0 -./.git/objects/33/5b7e601c09e5751a80f346c9aafb93d17101d2 -./.git/objects/72/05a1fdf91869d35795d35df9a44fcf0a365e3f -./.git/objects/c5/59d204968a27b32f5eb3235a91b07317749239 -./.git/objects/93/2d002794afabda8f14c595d1e9644943ca2757 -./.git/objects/44/5d1994a5ac57366078b198200a9e143d4ccda8 -./.git/objects/5f/d71bb7aba1ce2209373b626e99e8b7d6f4c03f -./.git/objects/0d/65d4ea9dbfe0e79f437e9efbd78c56ada25688 -./.git/objects/b9/3f0df8f29b3d8070ed2a9f54bbe31029fe66e1 -./.git/objects/b9/7cd799d77968fdc4cdfef22365468470cf0eb4 -./.git/objects/4f/fb116001eeded5674daef6abef65089208ed1a -./.git/objects/ff/b1e0654e644d404272145c9e82d40c70da3fcf -./.git/objects/fb/df8b0da5f9b93d54bb5f221c3bc97704675004 -./.git/objects/6b/680462719b3b632f1a37f89ce79005df57ef23 -./.git/objects/6b/ffc7308bc85dde17a094c88e4db4c055f72488 -./.git/objects/6b/1c053d37cecbcb581c74be40a7b0a76d450575 -./.git/objects/85/d23814e172addd8c22c0f8fa0bd2f2a62212f5 -./.git/objects/b6/098d3a5d03e998d50c9b87b06c3c1940ee7c4f -./.git/objects/bf/4855faf0508a152471f6c6c8f756581b8ebb96 -./.git/objects/bf/45cf47afbe1af29b37202ffa7aad670f724315 -./.git/objects/c8/2b2f7a810646753627819a01e54b2fbb0bf994 -./.git/objects/fa/64f14a857fee267529d204b1459599cdcd4c1c -./.git/objects/64/76ab8696fde6f30a60496b6432e722be4f7ecb -./.git/objects/64/bc3be12e5a19ba7b8d2feef676f87de847c7ee -./.git/objects/31/9ae2c46027c14596f9cbb2b7feac7638a53ac3 -./.git/objects/63/5ac650b9e5cb5102933f3b31acd333c5a80893 -./.git/objects/63/e6828bfe6daec53b28903951dba03be01d3030 -./.git/objects/79/eb1ce74e5351a6f490e0982cbc6e129b4aa7c8 -./.git/objects/5c/ca0f79b82611f512e6294a0f00596a234eb85c -./.git/objects/b3/16e45895dfd681cc04ee3301c329a74fdba090 -./.git/objects/1f/8147d384285cfe5ff18e96ebeff64aca142ab2 -./.git/objects/3b/4c4f812a8d80d92927085740fee2bafe5a602f -./.git/objects/23/16ac2bb4ceac9df62c1ff1e1a8a7bdcb01e5d7 -./.git/objects/9b/3926088eae0e151326f6f0015829521df010b4 -./.git/objects/27/8591987f33b59eb256926e6d9c96a0cdb40b92 -./.git/objects/d5/a0cd040e7aba6e44e5cb6c48b3b31de7fa3996 -./.git/objects/d5/61671aaa8c1913d335a6750e9fad59f6065448 -./.git/objects/d5/d06687674c7ac43f59839c7d16ffe001f7d0c7 -./.git/objects/d5/8b47cc8995eda0d0902699f1037849b620e723 -./.git/objects/d5/4fbe58d8f2468a576a9cec2bcaaa26801f6b3d -./.git/objects/d5/45ab9b4098ec8c1818861356b9fe334552af1a -./.git/objects/fe/ea2538a9529bd851c99b976099e226409080c3 -./.git/objects/fe/4849b2f3a88b6f3997f565ad7d9da97c402a58 -./.git/objects/fe/b44f5214740a873ac5eedbb942834d8916e07d -./.git/objects/55/3aa665e46a83e53e331b2435e4eacd6e696ba0 -./.git/objects/55/b6eb9fc430d168b02cbf5a231d93bb7632ab31 -./.git/objects/a0/e4208fa05edf37e2f64955495172d860048b74 -./.git/objects/7e/db6adeb6c45b5b3524c1c33d2c1abdfd2ef4b3 -./.git/objects/c4/507da960010d98b9560a34fe55686858f1116b -./.git/objects/38/a157dfa5222bb8aad0c3531a07c0eb288a9f0a -./.git/objects/b8/15446f819ac2dcba50e63e5781e8a1be9b59c6 -./.git/objects/74/d094765ec06d081a8eb0cf62a2e6f3812829a0 -./.git/objects/d4/7bcee05a434c6b87726710fcc854971a72d606 -./.git/objects/09/cad2ec21e916c54b3aebb07431df19760c0e25 -./.git/objects/a4/a10b59d02c8c64af41d0bb0255873e888ea89d -./.git/objects/e0/1a219d20f0a4e2a6fb4747565f072e89dbe3ee -./.git/objects/53/27cdf631d950ea1e10d0517bdcd67179e9721f -./.git/objects/37/30fb786f1b523cd179652a96f821ce88eea36d -./.git/objects/37/650c30d77f69b0b011040293e053a4e7c6f98c -./.git/objects/28/f953b838d0200c67301cc7f3a0d352e26f3c42 -./.git/objects/f2/d19117ef0cd800c52fde833c32acb71e46a664 -./.git/objects/4c/5dd0a4298e91212e5422fa71254fc0e5176158 -./.git/objects/4c/be96ff54343b2f28a2b5069343ec49a4342ba9 -./.git/objects/a8/d605fb18e5c5ee1f3f5352c1460e1f760535bd -./.git/objects/a8/d7d38e73f05bf78472f59a092bdef3949483e7 -./.git/objects/b0/165037a79df96cdc3570f14250ae79bb3e693e -./.git/objects/b0/af64a3d1d8dc88b08bb9208dea14f8761f973f -./.git/objects/2b/38518e1f2a3349a23737edb17748c0ec1428cc -./.git/objects/2b/52a895406f2999f2fed4bbb21dcd33d7569d6a -./.git/objects/b4/80245fb3f89a446b36d21c8b1b6df792e2e5a8 -./.git/objects/7a/6d78346ba3078860f945a4f56f88a8e50d2b42 -./.git/objects/58/f8841a3fc96b9e14be040842de44b85e5276cc -./.git/objects/b1/8e4237f7f9851fbf4180cd4d0549497c165637 -./.git/objects/b1/afe94b6b0a3bd565aabc05acd34e487b914071 -./.git/objects/c1/df659fb1b5539c087fa05da398c1ffa81712b8 -./.git/objects/c1/eb94b7d6938b2ec524f98d5b6ea9276139d26d -./.git/objects/c1/b79b5e4a0d723e1c8a437c21bba6846b39a9c8 -./.git/objects/81/3380a694d4f4961a9feea10d881c3554d41155 -./.git/objects/81/cedd6d7915b885fefd382950534d4d5ee4d2de -./.git/objects/e2/d340bf4d50c22789fcae8145de3d53e9487bc8 -./.git/objects/e2/2ce8b7aa5354f50987e9646e7b20274d7d8f3f -./.git/objects/e2/6091b8dfab7cd3efd7f0ce8f7b1dc8bd540f6f -./.git/objects/9e/73b99fe785e2c18588575ebea9e63409e6390b -./.git/objects/34/3455561e55393181b9ad342eedb62b242a59ca -./.git/objects/2d/9823652e7ca859d5f826b475c4d7a9bc979919 -./.git/logs/HEAD -./.git/logs/refs/heads/main -./.git/logs/refs/heads/develop -./.git/logs/refs/heads/feat/25-tid-improve-mapping-of-slice-requests -./.git/logs/refs/heads/feat/24-tid-restconf-base-nbi -./.git/logs/refs/remotes/origin/sat_module -./.git/logs/refs/remotes/origin/HEAD -./.git/logs/refs/remotes/origin/develop -./.git/logs/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests -./.git/logs/refs/remotes/origin/feat/24-tid-restconf-base-nbi -./.git/refs/heads/main -./.git/refs/heads/develop -./.git/refs/heads/feat/25-tid-improve-mapping-of-slice-requests -./.git/refs/heads/feat/24-tid-restconf-base-nbi -./.git/refs/remotes/origin/sat_module -./.git/refs/remotes/origin/HEAD -./.git/refs/remotes/origin/develop -./.git/refs/remotes/origin/feat/25-tid-improve-mapping-of-slice-requests -./.git/refs/remotes/origin/feat/24-tid-restconf-base-nbi -./.git/info/exclude -./.git/hooks/post-update.sample -./.git/hooks/pre-push.sample -./.git/hooks/sendemail-validate.sample -./.git/hooks/pre-receive.sample -./.git/hooks/pre-merge-commit.sample -./.git/hooks/pre-rebase.sample -./.git/hooks/update.sample -./.git/hooks/pre-applypatch.sample -./.git/hooks/fsmonitor-watchman.sample -./.git/hooks/push-to-checkout.sample -./.git/hooks/applypatch-msg.sample -./.git/hooks/pre-commit.sample -./.git/hooks/commit-msg.sample -./.git/hooks/prepare-commit-msg.sample diff --git a/out-updated.txt b/out-updated.txt deleted file mode 100644 index 6953feb..0000000 --- a/out-updated.txt +++ /dev/null @@ -1,88 +0,0 @@ -./Dockerfile -./.gitignore -./app.py -./deploy.sh -./.dockerignore -./.gitlab-ci.yml -./requirements.txt -./src/main.py -./src/mapper/process_connnectivity.py -./src/mapper/main.py -./src/mapper/get_service_template.py -./src/mapper/get_template.py -./src/mapper/slo_viability.py -./src/mapper/extract_sdp_info.py -./src/realizer/nrp_handler.py -./src/realizer/main.py -./src/realizer/send_controller.py -./src/realizer/select_way.py -./src/realizer/ixia/ixia_connect.py -./src/realizer/ixia/main.py -./src/realizer/ixia/helpers/automatizacion_ne2v4.py -./src/realizer/ixia/helpers/NEII_V4.py -./src/realizer/restconf/restconf_connect.py -./src/realizer/restconf/main.py -./src/realizer/restconf/connectors/tfs_connector.py -./src/realizer/restconf/connectors/frr_connector.py -./src/realizer/restconf/connectors/cisco_connector.py -./src/realizer/restconf/service_types/l3vpn.py -./src/realizer/restconf/service_types/l2vpn.py -./src/realizer/restconf/service_types/builders/configure_match_criteria.py -./src/realizer/restconf/service_types/builders/create_network_access.py -./src/realizer/restconf/service_types/builders/configure_slos.py -./src/realizer/restconf/service_types/builders/create_site_from_sdp.py -./src/realizer/restconf/service_types/builders/initialize_structure.py -./src/realizer/restconf/service_types/builders/apply_metric_constraint.py -./src/realizer/e2e/e2e_connect.py -./src/realizer/e2e/main.py -./src/realizer/e2e/service_types/del_l3ipowdm_slice.py -./src/realizer/e2e/service_types/l3ipowdm_slice.py -./src/realizer/tfs/tfs_connect.py -./src/realizer/tfs/main.py -./src/realizer/tfs/helpers/tfs_connector.py -./src/realizer/tfs/helpers/cisco_connector.py -./src/realizer/tfs/service_types/tfs_l2vpn.py -./src/realizer/tfs/service_types/tfs_l3vpn.py -./src/planner/planner.py -./src/planner/tfs_optical_planner/tfs_optical.py -./src/planner/hrat_planner/hrat.py -./src/planner/energy_planner/energy.py -./src/tests/test_e2e.py -./src/tests/test_mapper.py -./src/tests/test_api.py -./src/tests/test_utils.py -./src/tests/test_nbi_processor.py -./src/tests/test_database.py -./src/tests/test_initialization.py -./src/config/config.py -./src/config/.env.example -./src/config/constants.py -./src/database/store_data.py -./src/database/db.py -./src/database/sysrepo_store.py -./src/nbi_processor/detect_format.py -./src/nbi_processor/main.py -./src/nbi_processor/translator.py -./src/api/main.py -./src/webui/gui.py -./src/webui/templates/dev.html -./src/webui/templates/search.html -./src/webui/templates/login.html -./src/webui/templates/index.html -./src/webui/templates/welcome.html -./src/webui/templates/ixia.html -./src/utils/build_response.py -./src/utils/send_response.py -./src/utils/load_template.py -./src/utils/safe_get.py -./src/utils/dump_templates.py -./src/utils/slice_manager.py -./swagger/tfs_namespace.py -./swagger/ixia_namespace.py -./swagger/restconf_namespace.py -./swagger/E2E_namespace.py -./swagger/models/create_models_restconf.py -./swagger/models/create_models.py -./scripts/update_license_headers.py -./scripts/dump_templates_nsc.sh -./scripts/show_logs_nsc.sh -- GitLab From eb4e9c34eac197aa5ffd77ecb6fffa2b7866c07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Vel=C3=A1zquez?= Date: Wed, 6 May 2026 15:58:06 +0000 Subject: [PATCH 21/27] L3VPN Add routing protocols and vlan match Network access: change custommer address, site role and access type L2VPN Change initial structure --- .../restconf/service_types/builders/configure_match_criteria.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index af7c8e9..7f31701 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -28,6 +28,7 @@ def configure_match_criteria(network_access, site, sdp, layer_type): if layer_type == "l3": MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" + MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: -- GitLab From fc1e655180ddc0d433de3d99ddf284a4b499c642 Mon Sep 17 00:00:00 2001 From: velazquez Date: Fri, 8 May 2026 12:02:34 +0000 Subject: [PATCH 22/27] Add match type "any" --- .../restconf/service_types/builders/configure_match_criteria.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index 7f31701..a864b28 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -29,6 +29,7 @@ def configure_match_criteria(network_access, site, sdp, layer_type): MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too + MATCH_TYPE_MAPPING["any"] = "any" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: -- GitLab From 69af8bd2fce79e1af4e6d4c9447102dff7b192de Mon Sep 17 00:00:00 2001 From: velazquez Date: Fri, 8 May 2026 12:57:17 +0000 Subject: [PATCH 23/27] Add vlan match to l2 services --- .../restconf/service_types/builders/configure_match_criteria.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/realizer/restconf/service_types/builders/configure_match_criteria.py b/src/realizer/restconf/service_types/builders/configure_match_criteria.py index a864b28..af7c8e9 100644 --- a/src/realizer/restconf/service_types/builders/configure_match_criteria.py +++ b/src/realizer/restconf/service_types/builders/configure_match_criteria.py @@ -28,8 +28,6 @@ def configure_match_criteria(network_access, site, sdp, layer_type): if layer_type == "l3": MATCH_TYPE_MAPPING["source-ip-prefix"] = "ipv4-src-prefix" MATCH_TYPE_MAPPING["destination-ip-prefix"] = "ipv4-dst-prefix" - MATCH_TYPE_MAPPING["vlan"] = "vlan" # Need to check if applies with l2 too - MATCH_TYPE_MAPPING["any"] = "any" # Need to check if applies with l2 too match_criteria = sdp.get("match_criteria") if not match_criteria: -- GitLab From 2adc2cb473b5dfac80d1bf22d1e2a51527ad474b Mon Sep 17 00:00:00 2001 From: velazquez Date: Tue, 19 May 2026 13:39:16 +0200 Subject: [PATCH 24/27] Service_db creation with all functions to access de database Modify slice creation and deletion to also change the service database --- src/config/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/.env.example b/src/config/.env.example index 977a874..1e45691 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -66,7 +66,7 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=192.168.27.165 +RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO -- GitLab From 3d5e811ec2d2d889c92d118dbdf1328db6abc528 Mon Sep 17 00:00:00 2001 From: velazquez Date: Mon, 15 Jun 2026 10:49:06 +0200 Subject: [PATCH 25/27] Move postman collections to tests --- .DS_Store | Bin 6148 -> 0 bytes app.py | 2 +- src/.DS_Store | Bin 6148 -> 0 bytes .../collections}/NSC.postman_collection.json | 0 .../collections}/NSC.postman_environment.json | 0 5 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .DS_Store delete mode 100644 src/.DS_Store rename {PM Collections => src/tests/collections}/NSC.postman_collection.json (100%) rename {PM Collections => src/tests/collections}/NSC.postman_environment.json (100%) diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 0387394593eb6a564c8dbd9de88228aceaa7c799..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLftjJ0p@<=mA&7utoqAWI#~@svK0^gR5djNE-sg2WcN|2*3hplpYO%0SEyAmZ=eR diff --git a/app.py b/app.py index 2104af8..a2f162f 100644 --- a/app.py +++ b/app.py @@ -60,7 +60,7 @@ def create_app(): init_service() app = Flask(__name__) app = create_config(app) - app.logger.setLevel(logging.INFO) + # app.logger.setLevel(logging.INFO) CORS(app) # Configure logging to provide clear and informative log messages diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 45e350a5457c5e7e299d845881b71036b878945f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8z45|#6fRTZLfrY`H!I8m}!7(kRI5{alKL@Ie2}*<1 zFoMiLlZQ%=QllX-8UmvsFd71*Aut*OgD3<*^){&f=N?3E8gbS3Ltr!nhHD5gvbY4hIDsln96kiqxd~7? z5F1n#GlJ@5h#qiN3~K~1K?W2hpvpnjJ-8}ngtQ?*e316hh5#&pM(NQI7=RD}07=Rc A2><{9 diff --git a/PM Collections/NSC.postman_collection.json b/src/tests/collections/NSC.postman_collection.json similarity index 100% rename from PM Collections/NSC.postman_collection.json rename to src/tests/collections/NSC.postman_collection.json diff --git a/PM Collections/NSC.postman_environment.json b/src/tests/collections/NSC.postman_environment.json similarity index 100% rename from PM Collections/NSC.postman_environment.json rename to src/tests/collections/NSC.postman_environment.json -- GitLab From fce0e3746dae070d9df3690b32c2c44128ca9701 Mon Sep 17 00:00:00 2001 From: velazquez Date: Thu, 2 Jul 2026 15:56:13 +0200 Subject: [PATCH 26/27] Changes: - Add telemetry, client and subscription database and remove temporal databases - Simplify restconf namespace endpoints - Modify client functions in api main - Modify subscription functions in api main - Modify telemetry functions in api main - Move functions in api to specific mapper and realizer functions - Create workflow for monitoring in main - Create aggregate_monitoring function in mapper - Create get metrics function in realizer - Move functions for retrieving simap telemetry to tfs_connector function - Add new paths for simap telemetry retrieval and for ietf networks - Move telemetry cache into config.py - Modifications in postman collections for testing --- app.py | 2 + src/api/main.py | 778 ++++++------------ src/config/.env.example | 9 +- src/config/config.py | 5 + src/config/constants.py | 2 + src/database/telemetry_client_db.py | 314 +++++++ src/main.py | 39 +- src/mapper/aggregate_monitoring.py | 64 ++ src/mapper/main.py | 240 +++--- src/planner/shortest_path.py | 93 +++ src/realizer/get_metrics.py | 35 + src/realizer/main.py | 100 ++- .../restconf/connectors/tfs_connector.py | 168 +++- .../collections/NSC.postman_collection.json | 328 +++++--- .../collections/NSC.postman_environment.json | 6 +- swagger/restconf_namespace.py | 258 +++--- 16 files changed, 1511 insertions(+), 930 deletions(-) create mode 100644 src/database/telemetry_client_db.py create mode 100644 src/mapper/aggregate_monitoring.py create mode 100644 src/planner/shortest_path.py create mode 100644 src/realizer/get_metrics.py diff --git a/app.py b/app.py index a2f162f..2812a13 100644 --- a/app.py +++ b/app.py @@ -27,6 +27,7 @@ from src.webui.gui import gui_bp from src.config.config import create_config from src.database.db import init_db as init_slice from src.database.service_db import init_db as init_service +from src.database.telemetry_client_db import init_db as init_telemetry # Paths that do not require authentication (Swagger UI and its static assets) # /nsc → Swagger UI @@ -58,6 +59,7 @@ def create_app(): """Create Flask application with configured API and namespaces.""" init_slice() init_service() + init_telemetry() app = Flask(__name__) app = create_config(app) # app.logger.setLevel(logging.INFO) diff --git a/src/api/main.py b/src/api/main.py index ed1e165..c778aa6 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -14,50 +14,23 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import json -import time -from typing import Dict, List, Tuple -import uuid - -import requests - from src.utils.send_response import send_response -import logging +import logging, json, time, requests, traceback, asyncio, aiohttp 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 +from src.database.telemetry_client_db import create_client, get_client, get_all_clients, delete_client, delete_all_clients, upsert_subscription, get_subscription, get_client_subscriptions, delete_subscription, delete_all_subscriptions from src.realizer.tfs.helpers.tfs_connector import tfs_connector 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 collections import deque -import traceback - -import asyncio -import aiohttp +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 class Api: - NSC_BASE_URL = "http://192.168.27.189" - SDN_TOPOLOGY_URL = f"{NSC_BASE_URL}/restconf/data/ietf-network:networks" - SDN_SUBSCRIBE_URL = f"{NSC_BASE_URL}/restconf/operations/subscriptions:establish-subscription" - SDN_SUBSCRIPTION_PERIOD = 10 # seconds - SDN_SUBS_INACTIVITY_THRESHOLD = 30 # seconds def __init__(self, slice_service): self.slice_service = slice_service - if "slices" not in current_app.config: current_app.config["slices"] = {} - if "telemetryClients" not in current_app.config: current_app.config["telemetryClients"] = {} - if "slices" not in current_app.config: current_app.config["slices"] = {} - self.telemetry_cache = {} - self.session = None #For connections with SDN - - async def get_session(self): - # Crea la sesión si no existe - if self.session is None or self.session.closed: self.session = aiohttp.ClientSession() - return self.session - - async def close_session(self): - # Llama a esto al apagar tu app - if self.session: await self.session.close() def add_flow(self, intent): """ @@ -335,15 +308,6 @@ class Api: result = self.slice_service.nsc(intent) if result: create_data_store(intent) - - slos = safe_get(intent, ['ietf-network-slice-service:network-slice-services', 'slo-sle-templates', 'slo-sle-template'], []) - slosDict = { slo["id"]: { "slo-policy": slo["slo-policy"], "sle-policy": slo["sle-policy"] } for slo in slos } - for slice in result["slices"]: - current_app.config["slices"][slice["id"]] = { - "src": slice["source"], - "dst": slice["destination"], - "slo-sle-policy": slosDict.get(slice.get("slo-sle-template", ""), {}), - } logging.info(f"Network Slice created successfully") return send_response( True, @@ -669,7 +633,6 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) - current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -726,7 +689,6 @@ class Api: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) - current_app.config["slices"].pop(slice_id, None) logging.info(f"Slice {slice_id} removed successfully") return {}, 204 @@ -749,7 +711,6 @@ class Api: if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) - current_app.config["slices"].clear() logging.info("All slices removed successfully") return {}, 204 @@ -789,489 +750,223 @@ class Api: return send_response(False, code=404, message=str(e)) except Exception as e: return send_response(False, code=500, message=str(e)) - - def getShortestPath(self, network, srcNodeId, dstNodeId, directedGraph: bool = False) -> Tuple[Dict[str, any], int]: - try: - sourceNodeIdx = srcNodeId - destNodeIdx = dstNodeId - nodes = network["node"] - links = network["ietf-network-topology:link"] - - # Construir grafo (no dirigido) - graph = {node["node-id"]: set() for node in nodes} - for link in links: - src = link["source"]["source-node"] - dst = link["destination"]["dest-node"] - - graph[src].add(dst) - if not directedGraph: graph[dst].add(src) - - # BFS - visited = { node["node-id"]: False for node in nodes } - prev = { node["node-id"]: -1 for node in nodes } - - queue = deque([sourceNodeIdx]) - visited[sourceNodeIdx] = True - - while queue: - current = queue.popleft() - if current == destNodeIdx: break - - for neighbor in graph[current]: - if not visited[neighbor]: - visited[neighbor] = True - prev[neighbor] = current - queue.append(neighbor) - - # Reconstruir camino - path = [] - at = destNodeIdx - if not visited[destNodeIdx]: return {"message": "No path found"}, 404 - - while at != -1: - path.append(at) - at = prev[at] - path.reverse() - return path, 200 - except Exception as e: - logging.info(f"Error calculating shortest path: {traceback.print_exc()}") - return {"message": str(e)}, 500 - # --- SDN STREAMS --- + # --- CLIENTS --- - async def listenStream(self, session, link_id, slice_id, stream_url): + def get_clients(self, client_id=None): try: - async with session.get( - stream_url, - auth=aiohttp.BasicAuth("admin", "admin"), - timeout=None, - headers = { - "Accept": "text/event-stream", - "Cache-Control": "no-cache" - } - ) as resp: - buffer = "" - - logging.info(f"Started listening to stream for link {link_id} in slice {slice_id}") - - async for chunk in resp.content.iter_any(): - buffer += chunk.decode() - - # SSE separa los eventos con dos saltos de línea (\n\n) - while "\n\n" in buffer: - # Extraemos el evento completo y dejamos el resto en el buffer - event_block, buffer = buffer.split("\n\n", 1) - - for line in event_block.split("\n"): - if line.startswith("data:"): - json_str = line.replace("data:", "").strip() - - try: - data = json.loads(json_str) - logging.info(f"JSON detected: {data}") - - telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] - bw = telemetry.get("bandwidth-utilization", "N/A") - latency = telemetry.get("latency", "N/A") - self.telemetry_cache.setdefault(slice_id, {})[link_id] = { - "timestamp": data["notification"]["eventTime"], - "bandwidth": float(bw) if bw != "N/A" else bw, - "latency": float(latency) if latency != "N/A" else latency, - "services": telemetry.get("related-service-ids", []) - } - except json.JSONDecodeError: logging.error("Error parsing the JSON from field 'data'") - except Exception as e: logging.error(f"Error processing logic: {e}") - except aiohttp.ClientError as e: logging.error(f"Connection error with stream for link {link_id}: {e}") - logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") - - async def startStreams(self, links, slice_id): - #async with self.get_session() as session: #timeout = aiohttp.ClientTimeout(total=10) - session = await self.get_session() - tasks = [] - - for link in links: - sub_resp = await session.post(Api.SDN_SUBSCRIBE_URL, json={ - "ietf-subscribed-notifications:input": { - "datastore": "operational", - "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", - "ietf-yang-push:periodic": { - "ietf-yang-push:period": Api.SDN_SUBSCRIPTION_PERIOD - } - } - }, - auth=aiohttp.BasicAuth("admin", "admin") - ) - sub_data = await sub_resp.json() - logging.info(f"Subscription response for link {link} in slice {slice_id}: {sub_data}") - """ - response format is: - { - "id": 1161739417341820929, - "uri": "/restconf/stream/1161739417341820929" - } - """ - - if "uri" in sub_data: - tasks.append( - asyncio.create_task( - self.listenStream(session, link, slice_id, Api.NSC_BASE_URL + sub_data['uri']) - ) - ) - #await asyncio.gather(*tasks) + if client_id: + return get_client(client_id), 200 + clients = get_all_clients() + if not clients: + raise ValueError("No clients found") + return clients, 200 - # --- CLIENTS --- - - def createClient(self, clientId): - response, code = self.getClient(clientId) - if code == 400: return response, code - if code == 200: return {"message": f"Client '{clientId}' already exists"}, 400 - - current_app.config["telemetryClients"][clientId] = { "subscriptions": {} } - return {"message": f"Client '{clientId}' created successfully"}, 200 - - def deleteClient(self, clientId): - response, code = self.getClient(clientId) - if response != 200: return response, code + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - del current_app.config["telemetryClients"][clientId] - return {"message": f"Client '{clientId}' deleted successfully"}, 200 - - def getClient(self, clientId) -> Tuple[Dict[str,any], int]: - if not clientId: return {"message": "clientId is required"}, 400 - clients = current_app.config["telemetryClients"] + def add_client(self, client_id): + try: + create_client(client_id) + logging.info(f"Client '{client_id}' created successfully") + return send_response( + True, + code=201, + message=f"Client '{client_id}' created successfully", + data={"client_id": client_id} + ) - if clientId not in clients: return {"message": f"Client '{clientId}' not found"}, 404 - return clients[clientId], 200 + except ValueError as e: + return send_response(False, code=409, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - def getAllClients(self) -> Tuple[Dict[str,any], int]: return current_app.config["telemetryClients"], 200 + def delete_clients(self, client_id=None): + try: + if client_id: + delete_client(client_id) + logging.info(f"Client '{client_id}' removed successfully") + else: + delete_all_clients() + logging.info("All clients removed successfully") + return {}, 204 + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - # --- TELEMETRY --- + # --- SUBSCRIPTIONS --- - async def getNetworkTopology(self, sliceId) -> Tuple[Dict[str, any], int]: - # MOCKED TOPOLOGY - topology = requests.get(Api.SDN_TOPOLOGY_URL, auth=("admin", "admin")) - networkRaw = topology.json() + async def get_subscriptions(self, client_id, slice_id = None): + try: + if slice_id is not None: + try: + subscription = get_subscription(client_id, slice_id) + except ValueError: + subscription = None + if not subscription: + raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") + + telemetry = await self.get_telemetry(slice_id) - """ - topology = [ - { - "ietf-network:networks": { - "network": [ - { - "ietf-network-topology:link": [ - { - "destination": { - "dest-node": "AMF-N2", - "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" - }, - "link-id": "4.4.4.4-AMF-N2", - "source": { - "source-node": "4.4.4.4", - "source-tp": "0/0/2-GigabitEthernet0/0/0/2" - } - }, - { - "destination": { - "dest-node": "1.1.1.1", - "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" - }, - "link-id": "4.4.4.4-1.1.1.1", - "source": { - "source-node": "4.4.4.4", - "source-tp": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "destination": { - "dest-node": "3.3.3.3", - "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" - }, - "link-id": "CU-N2-3.3.3.3", - "source": { - "source-node": "CU-N2", - "source-tp": "0/0/1-GigabitEthernet0/0/0/1" - } - }, - { - "destination": { - "dest-node": "4.4.4.4", - "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" - }, - "link-id": "1.1.1.1-4.4.4.4", - "source": { - "source-node": "1.1.1.1", - "source-tp": "0/0/1-GigabitEthernet0/0/0/1" - } - }, - { - "destination": { - "dest-node": "CU-N2", - "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" - }, - "link-id": "1.1.1.1-CU-N2", - "source": { - "source-node": "1.1.1.1", - "source-tp": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "destination": { - "dest-node": "4.4.4.4", - "dest-tp": "0/0/2-GigabitEthernet0/0/0/2" - }, - "link-id": "AMF-N2-4.4.4.4", - "source": { - "source-node": "AMF-N2", - "source-tp": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "destination": { - "dest-node": "CU-N2", - "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" - }, - "link-id": "3.3.3.3-CU-N2", - "source": { - "source-node": "3.3.3.3", - "source-tp": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "destination": { - "dest-node": "4.4.4.4", - "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" - }, - "link-id": "3.3.3.3-4.4.4.4", - "source": { - "source-node": "3.3.3.3", - "source-tp": "0/0/1-GigabitEthernet0/0/0/1" - } - }, - { - "destination": { - "dest-node": "3.3.3.3", - "dest-tp": "0/0/1-GigabitEthernet0/0/0/1" - }, - "link-id": "4.4.4.4-3.3.3.3", - "source": { - "source-node": "4.4.4.4", - "source-tp": "0/0/1-GigabitEthernet0/0/0/1" - } - }, - { - "destination": { - "dest-node": "1.1.1.1", - "dest-tp": "0/0/0-GigabitEthernet0/0/0/0" - }, - "link-id": "CU-N2-1.1.1.1", - "source": { - "source-node": "CU-N2", - "source-tp": "0/0/0-GigabitEthernet0/0/0/0" - } - } - ], - "network-id": "slice_1 MOCKED", - "network-types": { - "ietf-l3-unicast-topology:l3-unicast-topology": {} - }, - "node": [ - { - "ietf-l3-unicast-topology:l3-node-attributes": { - "name": "CU-N2" - }, - "node-id": "CU-N2" - }, - { - "ietf-l3-unicast-topology:l3-node-attributes": { - "name": "AMF-N2" - }, - "node-id": "AMF-N2" - }, - { - "ietf-l3-unicast-topology:l3-node-attributes": { - "name": "1.1.1.1" - }, - "node-id": "1.1.1.1" - }, - { - "ietf-l3-unicast-topology:l3-node-attributes": { - "name": "4.4.4.4" - }, - "node-id": "4.4.4.4" - }, - { - "ietf-l3-unicast-topology:l3-node-attributes": { - "name": "3.3.3.3" - }, - "node-id": "3.3.3.3" - } - ] - } - ] - } - } - ] if True else (await response.json()) - #""" - - network = next((n["ietf-network:networks"]["network"][0] for n in networkRaw if n["ietf-network:networks"]["network"][0]["network-id"] == "admin"), None) - logging.info(f"Retrieved topology for slice '{sliceId}': {network}") - - return network, 200 - - async def getTelemetry(self, sliceId, subscribe: bool = False) -> Dict[str, any]: - if sliceId in current_app.config["slices"]: - #Retrieve Network topology: - config = current_app.config["slices"][sliceId] - if "topology" not in config or config["topology"] is None: - topology, code = await self.getNetworkTopology(sliceId) #Currently mocked - if code != 200: return { "message": f"There was an error retrieving topology for slice '{sliceId}': {topology}" } - config["topology"] = topology - else: topology = config["topology"] - - #Get shortest path from source node to destination node: - if "shortestPath" not in config or config["shortestPath"] is None: - shortestPath, code = self.getShortestPath(topology, config["src"], config["dst"]) - logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") - if code != 200: return shortestPath - config["shortestPath"] = shortestPath - else: shortestPath = config["shortestPath"] - - # Make sure data is already initialized in cache - # (this will only happen if an already-opened stream with SDN controller has sent data for this slice, - # otherwise it will be initialized when the first data arrives): - if sliceId in self.telemetry_cache: - """ the format is the following: - self.telemetry_cache[sliceId][linkId] = { - "timestamp": str, #time.time(), - "bandwidth": float, - "latency": float, - "services": List[any] - } - """ + return { + **subscription, + "telemetry": telemetry + }, 200 - slo = current_app.config["slices"][sliceId]["slo-sle-policy"].get("slo-policy", {}) - sloBandwith = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-bandwidth" ][0] if slo else 0 - sloLatency = [ s["bound"] for s in slo.get("metric-bound", []) if s.get("metric-type", "") == "one-way-delay-maximum" ][0] if slo else "N/A" + subscriptions = get_client_subscriptions(client_id) - bw = min([link["bandwidth"] for _, link in self.telemetry_cache[sliceId].items()]) - lt = sum([link["latency"] for _, link in self.telemetry_cache[sliceId].items()]) + result = [] - warningJson = { "warning": {} } - if sloLatency != "N/A" and lt > float(sloLatency): warningJson["warning"]["latency"] = f"High latency: {lt}ms exceeds SLO of {sloLatency}ms" - if sloBandwith != 0 and bw > float(sloBandwith): warningJson["warning"]["bandwidth"] = f"High bandwidth utilization: {bw}Kb/s exceeds SLO of {sloBandwith}Kb/s" - return { - "sliceId": sliceId, - "telemetry": { - "desc": f"Aggregated telemetry of network slice {sliceId}. Other additional fields could be included in the future.", - "latency": f"{lt}ms", - "bandwidth": f"{bw}Mb/s" - } + warningJson, - "timestamp": time.time() - } - else: - if subscribe: # Open a stream with SDN controller for this slice: - shortestPathLinks = [] - if sliceId not in self.telemetry_cache: self.telemetry_cache[sliceId] = {} - for i in range(len(shortestPath)-1): - linkId = f"{shortestPath[i]}-{shortestPath[i+1]}" - shortestPathLinks.append(linkId) - self.telemetry_cache[sliceId][linkId] = { - "timestamp": None, #time.time(), - "bandwidth": 0, - "latency": 0, - "services": [] - } + for sub in subscriptions: + slice_id = sub["slice_id"] - # Get telemetry of every node in the slice: - """ - self.startStreams(self, [ - link["link-id"] - for link in topology["ietf-network-topology:link"] - ], sliceId) - """ + telemetry = await self.get_telemetry(slice_id) - # Get telemetry only of the links in the shortest path: - await self.startStreams(shortestPathLinks, sliceId) - return { "message": f"No telemetry available for slice '{sliceId}'" } - else: return { "message": f"There is no slice with id '{sliceId}' registered" } + result.append({ + **sub, + "telemetry": telemetry + }) - async def getAllTelemetry(self, subscribe: bool = False) -> Dict[str, Dict[str, any]]: - telemetryData = {} - for sliceId in current_app.config.get("slices", {}).keys(): telemetryData[sliceId] = await self.getTelemetry(sliceId, subscribe) - return telemetryData + return { + "client_id": client_id, + "subscriptions": result + }, 200 + + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - # --- SUBSCRIPTIONS --- + async def add_subscription(self, client_id, slice_id, frequency): + try: + try: + subscription = get_subscription(client_id, slice_id) + except ValueError: + subscription = None + if subscription: + raise ValueError(f"Client '{client_id}' already has a subscription for slice '{slice_id}'") + if not frequency: + raise KeyError("Field 'frequency' is required") + + upsert_subscription(client_id, slice_id, frequency) + logging.info(f"Subscription for slice '{slice_id}' and client '{client_id}' created successfully") + return send_response( + True, + code=201, + message="Subscription successfully created", + data={ + "sliceId": slice_id, + "frequency": frequency + } + ) + except KeyError as e: + return send_response(False, code=400, message=str(e)) + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) + + async def update_subscription(self, client_id, slice_id, frequency): + try: + try: + subscription = get_subscription(client_id, slice_id) + except ValueError: + subscription = None + if not subscription: + raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") + if not frequency: + raise KeyError("Field 'frequency' is required") + + upsert_subscription(client_id, slice_id, frequency) + logging.info(f"Subscription for slice '{slice_id}' and client '{client_id}' modified successfully") + return send_response( + True, + code=201, + message="Subscription successfully modified", + data={ + "sliceId": slice_id, + "frequency": frequency + } + ) + except KeyError as e: + return send_response(False, code=400, message=str(e)) + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - async def getSubscriptionJson(self, response, sliceId, subscribe: bool = False): - return { - "subscriptionFrequency": response["frequency"], - "sourceNode": current_app.config["slices"][sliceId]["src"] if sliceId in current_app.config["slices"] else "UNKNOWN", - "destinationNode": current_app.config["slices"][sliceId]["dst"] if sliceId in current_app.config["slices"] else "UNKNOWN", - "telemetry": await self.getTelemetry(sliceId, subscribe) - } - - async def getTelemetrySubscriptions(self, clientId, sliceId = None, subscribe: bool = False) -> Tuple[Dict[str,any], int]: - response, code = self.getClient(clientId) - if code != 200: return response, code - if sliceId is not None: - if sliceId not in response["subscriptions"]: return {"message": f"Client '{clientId}' has no subscription for slice '{sliceId}'"}, 400 + def delete_subscriptions(self, client_id, slice_id = None): + try: + subscriptions = get_client_subscriptions(client_id) + if slice_id: + if slice_id not in subscriptions: + raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") + delete_subscription(client_id, slice_id) + logging.info(f"Subscription for slice '{slice_id}' and client '{client_id}' removed successfully") + return {}, 204 + delete_all_subscriptions(client_id) + logging.info(f"All subscriptions for client '{client_id}' removed successfully") + return {}, 204 - return { - "clientId": clientId, - "sliceSubscription": await self.getSubscriptionJson(response["subscriptions"][sliceId], sliceId, subscribe) - }, 200 - else: - return { - "clientId": clientId, - "sliceSubscriptions": [ (await self.getSubscriptionJson(sub, sId, subscribe)) for sId, sub in response["subscriptions"].items() ] - }, 200 + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) - async def createTelemetrySubscription(self, clientId, sliceId, frequency, src, dst) -> Tuple[Dict[str,any], int]: - response, code = self.getClient(clientId) - if code != 200: return response, code - sliceExists = sliceId in response["subscriptions"] - - if not frequency: return {"message": "Field 'frequency' is required"}, 400 - - topology, code = await self.getNetworkTopology(sliceId) #Currently mocked - if code != 200: - msg = f"There was an error retrieving topology for slice '{sliceId}': {topology}" - logging.error(msg) - return { "message": msg }, code - - #Get shortest path from source node to destination node: - shortestPath, code = self.getShortestPath(topology, src, dst) - if code != 200: - logging.info(f"Error while computing shortest path for slice '{sliceId}': {shortestPath} (code: {code})") - return shortestPath, code - else: - logging.info(f"Shortest path for slice '{sliceId}': {shortestPath} (code: {code})") - - response["subscriptions"][sliceId] = { "frequency": frequency } - current_app.config["slices"][sliceId] = { - "src": src, - "dst": dst, - "topology": topology, - "shortestPath": shortestPath - } + # --- TELEMETRY --- - return { - "message": f"Subscription successfully {"modified" if sliceExists else "created"}", - "sliceId": sliceId, - "frequency": frequency #"clientId": str(uuid.uuid4()) - }, 201 - - def deleteTelemetrySubscriptions(self, clientId, sliceId = None) -> Tuple[Dict[str,any], int]: - response, code = self.getClient(clientId) - if code != 200: return response, code - if sliceId: - if sliceId not in response["subscriptions"]: return {"message": f"Subscription '{sliceId}' not found for client {clientId}"}, 404 - - del response["subscriptions"][sliceId] - return {"message": "Subscription deleted"}, 200 - else: - response["subscriptions"] = {} - return {"message": "All subscriptions deleted"}, 200 + async def get_telemetry(self, slice_id = None): + 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: + raise ValueError(f"There is no slice with id '{slice_id}' registered") + template_id = safe_get(existing_slice, ["network-slice-services", "slice-service", slice_id, "slo-sle-template"]) + slo_sle_template = self.get_slo_sle_templates(template_id)[0] + slo_sle_template = safe_get(slo_sle_template, ["network-slice-services", "slo-sle-templates", "slo-sle-template"]) + 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 = await self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) + return metrics, 200 + + telemetry_data = {} + slices_data = self.get_slice_services()[0] + slice_service_list = slices_data["network-slice-services"]["slice-service"] + if isinstance(slice_service_list, dict): + slice_service_list = list(slice_service_list.values()) + + for slice in slice_service_list: + selected_template_id = slice.get("slo-sle-template") + slo_sle_template = self.get_slo_sle_templates(selected_template_id)[0] + slo_sle_template = safe_get(slo_sle_template, ["network-slice-services", "slo-sle-templates", "slo-sle-template"]) + slo_sle_template = next(iter(slo_sle_template), None) + 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] = await self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) + return telemetry_data, 200 + + except ValueError as e: + return send_response(False, code=404, message=str(e)) + except Exception as e: + return send_response(False, code=500, message=str(e)) def sync_stream(self, async_gen_func, *args, **kwargs): loop = asyncio.new_event_loop() @@ -1283,46 +978,33 @@ class Api: except StopAsyncIteration: pass finally: loop.close() - async def openSubscriptions(self, clientId, sliceId = None): - try: - response, code = self.getClient(clientId) - if code != 200: yield f"event: error\ndata: {json.dumps(data)}\n\n" - else: - frequency = 0 - okToProceed = True - # Process only for one slice: - if sliceId: - if sliceId in response["subscriptions"]: - frequency = response["subscriptions"][sliceId]["frequency"] - yield f"data: Using frequency of {frequency}...\n\n" - else: - yield f"data: sliceId '{sliceId}' is not present within '{clientId}' subscriptions. Closing connection now...\n\n" - okToProceed = False - # Process for all slices: + async def stream_client_subscriptions(self, client_id): + while True: + try: + data, code = await self.get_subscriptions(client_id) + if code == 200: + yield f"data: {json.dumps(data)}\n\n" + subs = data.get("subscriptions", []) + freq = max([s["frequency"] for s in subs]) if subs else 5 + await asyncio.sleep(freq) + else: + yield f"event: error\ndata: {json.dumps(data)}\n\n" + break + except Exception as e: + yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n" + break + + async def stream_slice_subscription(self, client_id, slice_id): + while True: + try: + data, code = await self.get_subscriptions(client_id, slice_id) + if code == 200: + yield f"data: {json.dumps(data)}\n\n" + freq = data.get("frequency", 5) + await asyncio.sleep(freq) else: - # Using policy of highest frequency among the subscribed slices, but other policies could be implemented (e.g., average/min frequency, one stream per slice with its own frequency, etc.) - frequency = max([ sub["frequency"] for _, sub in response["subscriptions"].items() ]) if response["subscriptions"] else 0 - #for sliceId, sub in response["subscriptions"].items(): frequency = max(frequency, sub["frequency"]) - yield f"data: Using frequency of {frequency}...\n\n" - - while okToProceed: - try: - data, code = await self.getTelemetrySubscriptions(clientId, sliceId, True) - - if code == 200: - if "warning" in data["sliceSubscription"]["telemetry"]: - logging.warning(f"Warning for client '{clientId}' in slice '{sliceId}': {data['sliceSubscription']['telemetry']['warning']}") - yield f"event: warning\ndata: {json.dumps(data)}\n\n" - else: yield f"data: {json.dumps(data)}\n\n" - await asyncio.sleep(frequency) - else: - yield f"event: error\ndata: {json.dumps(data)}\n\n" - break - except aiohttp.ClientError as e: - errorStr = "Connection timeout to SDN Controller." if "Connection timeout to host" in str(e) else str(e) - yield f"event: error\ndata: {json.dumps({'error': errorStr})}\n\n" - break - except GeneratorExit: - current_app.logger.info(traceback.print_exc()) - #current_app.config["telemetryClients"].pop(clientId, None) - except Exception: current_app.logger.info(traceback.print_exc()) \ No newline at end of file + yield f"event: error\ndata: {json.dumps(data)}\n\n" + break + except Exception as e: + yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n" + break \ No newline at end of file diff --git a/src/config/.env.example b/src/config/.env.example index 1e45691..5b30a13 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -42,7 +42,7 @@ E2E_OPTICAL_IP=127.0.0.1 # Realizer # ------------------------- # If true, no config sent to controllers -DUMMY_MODE=false +DUMMY_MODE=true # ------------------------- # Teraflow @@ -66,12 +66,17 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=127.0.0.1 +RESTCONF_IP=192.168.27.189 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO DATAPLANE_SUPPORT=CISCO +# ------------------------- +# Monitoring +# ------------------------- +SDN_SUBSCRIPTION_PERIOD=10 + # ------------------------- # WebUI # ------------------------- diff --git a/src/config/config.py b/src/config/config.py index c059ede..2e5a9aa 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -69,8 +69,13 @@ def create_config(app: Flask): app.config["SDN_CONTROLLER_TYPE"] = os.getenv("SDN_CONTROLLER_TYPE", "TFS") app.config["DATAPLANE_SUPPORT"] = os.getenv("DATAPLANE_SUPPORT", "FRR") + # Monitoring + app.config["SDN_SUBSCRIPTION_PERIOD"] = int(os.getenv("SDN_SUBSCRIPTION_PERIOD", "10")) + app.config["TELEMETRY_CACHE"] = {} + # API app.config["API_USERNAME"] = os.getenv("API_USERNAME", "admin") app.config["API_PASSWORD"] = os.getenv("API_PASSWORD", "admin") return app + diff --git a/src/config/constants.py b/src/config/constants.py index 279d459..7bfeb69 100644 --- a/src/config/constants.py +++ b/src/config/constants.py @@ -28,6 +28,8 @@ DATABASE_PATH = SRC_PATH / "database" CONFIG_PATH = SRC_PATH / "config" NBI_L2_PATH = "restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services" NBI_L3_PATH = "restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services" +NBI_IETF_NETWORKS_PATH = "restconf/data/ietf-network:networks" +NBI_SIMAP_SUSCRIPTION_PATH = "/restconf/operations/subscriptions:establish-subscription" diff --git a/src/database/telemetry_client_db.py b/src/database/telemetry_client_db.py new file mode 100644 index 0000000..d9d2352 --- /dev/null +++ b/src/database/telemetry_client_db.py @@ -0,0 +1,314 @@ +# Copyright 2022-2026 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 sqlite3, logging, json + +DB_NAME = "telemetry_client.db" + + +def init_db(): + """ + Initialize database and create telemetry_client table if not exists. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS client ( + client_id TEXT PRIMARY KEY + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS client_slice_subscription ( + client_id TEXT, + slice_id TEXT, + frequency INTEGER NOT NULL, + + PRIMARY KEY (client_id, slice_id), + + FOREIGN KEY (client_id) REFERENCES client(client_id) ON DELETE CASCADE + ) + """) + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS telemetry ( + slice_id TEXT PRIMARY KEY, + + telemetry_list TEXT NOT NULL, + slo_sle_compliance TEXT NOT NULL, + + timestamp REAL NOT NULL + ) + """) + + conn.commit() + conn.close() + +def get_conn(): + conn = sqlite3.connect(DB_NAME) + conn.execute("PRAGMA foreign_keys = ON") + return conn + +def create_client(client_id: str): + conn = get_conn() + cursor = conn.cursor() + + try: + cursor.execute( + "INSERT INTO client (client_id) VALUES (?)", + (client_id,) + ) + conn.commit() + + except sqlite3.IntegrityError: + raise ValueError(f"Client '{client_id}' already exists") + + finally: + conn.close() + +def delete_client(client_id: str): + """ + Delete client. + """ + conn = get_conn() + cursor = conn.cursor() + + cursor.execute( + "DELETE FROM client WHERE client_id = ?", + (client_id,) + ) + + if cursor.rowcount == 0: + raise ValueError(f"No client found with id '{client_id}' to delete.") + + logging.debug(f"Client '{client_id}' deleted.") + + conn.commit() + conn.close() + + +def get_client(client_id: str) -> dict: + conn = get_conn() + cursor = conn.cursor() + + cursor.execute( + "SELECT * FROM client WHERE client_id = ?", + (client_id,) + ) + + row = cursor.fetchone() + + if not row: + conn.close() + raise ValueError(f"No client found with id '{client_id}'.") + + result = { + "client_id": row[0] + } + + conn.close() + + return result + + +def get_all_clients() -> list[dict]: + conn = get_conn() + cursor = conn.cursor() + + cursor.execute("SELECT client_id FROM client") + + rows = cursor.fetchall() + + conn.close() + + return [ + { + "client_id": row[0] + } + for row in rows + ] + + +def delete_all_clients(): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute("DELETE FROM client") + + conn.commit() + conn.close() + + logging.debug("All clients deleted.") + +def upsert_subscription(client_id: str, slice_id: str, frequency: int): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO client_slice_subscription (client_id, slice_id, frequency) + VALUES (?, ?, ?) + ON CONFLICT(client_id, slice_id) + DO UPDATE SET frequency = excluded.frequency + """, (client_id, slice_id, frequency)) + + conn.commit() + conn.close() + +def get_subscription(client_id: str, slice_id: str) -> dict: + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + SELECT client_id, slice_id, frequency + FROM client_slice_subscription + WHERE client_id = ? AND slice_id = ? + """, (client_id, slice_id)) + + row = cursor.fetchone() + conn.close() + + if not row: + raise ValueError("Subscription not found") + + return { + "client_id": row[0], + "slice_id": row[1], + "frequency": row[2] + } + +def get_client_subscriptions(client_id: str) -> list[dict]: + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + SELECT slice_id, frequency + FROM client_slice_subscription + WHERE client_id = ? + """, (client_id,)) + + rows = cursor.fetchall() + conn.close() + + return [ + { + "slice_id": r[0], + "frequency": r[1] + } + for r in rows + ] + +def delete_subscription(client_id: str, slice_id: str): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM client_slice_subscription + WHERE client_id = ? AND slice_id = ? + """, (client_id, slice_id)) + + if cursor.rowcount == 0: + raise ValueError("Subscription not found") + + conn.commit() + conn.close() + +def delete_all_subscriptions(client_id: str): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM client_slice_subscription + WHERE client_id = ? + """, (client_id,)) + + conn.commit() + conn.close() + +def upsert_telemetry( + slice_id: str, + telemetry_list: dict, + slo_sle_compliance: dict, + timestamp: float +): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO telemetry ( + slice_id, + telemetry_list, + slo_sle_compliance, + timestamp + ) + VALUES (?, ?, ?, ?) + + ON CONFLICT(slice_id) + DO UPDATE SET + telemetry_list = excluded.telemetry_list, + slo_sle_compliance = excluded.slo_sle_compliance, + timestamp = excluded.timestamp + """, ( + slice_id, + json.dumps(telemetry_list), + json.dumps(slo_sle_compliance), + timestamp + )) + + conn.commit() + conn.close() + +def get_telemetry(slice_id: str): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + SELECT + slice_id, + telemetry_list, + slo_sle_compliance, + timestamp + FROM telemetry + WHERE slice_id = ? + """, (slice_id,)) + + row = cursor.fetchone() + + conn.close() + + if not row: + raise ValueError( + f"No telemetry found for slice '{slice_id}'" + ) + + return { + "slice_id": row[0], + "telemetry_list": json.loads(row[1]), + "slo_sle_compliance": json.loads(row[2]), + "timestamp": row[3] + } + +def delete_telemetry(slice_id: str): + conn = get_conn() + cursor = conn.cursor() + + cursor.execute(""" + DELETE FROM telemetry + WHERE slice_id = ? + """, (slice_id,)) + + conn.commit() + conn.close() \ No newline at end of file diff --git a/src/main.py b/src/main.py index ba64187..069caf4 100644 --- a/src/main.py +++ b/src/main.py @@ -22,6 +22,7 @@ from src.nbi_processor.main import nbi_processor from src.database.store_data import store_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: @@ -95,7 +96,7 @@ class NSController: ietf_intents = nbi_processor(intent_json) for intent in ietf_intents: - logging.info(intent) + logging.debug(intent) # Mapper services, rules = mapper(intent, controller_type=self.controller_type) logging.debug(f"Services: {services}") @@ -129,4 +130,38 @@ class NSController: return { "slices": self.response, "setup_time": setup_time - } \ No newline at end of file + } + + async def monitoring(self, slice_id, slo_sle_template, sdps): + """ + Monitor the status of a specific network slice. + + Workflow: + 1. Validate the provided slice_id + 2. Request the planner the shortest path for the slice + 3. Request the realizer to retrieve the metrics for the links of the specified slice + 4. Realizer retrieves the metrics from the controller for the specified slice + 5. Request mapper to aggregate the metrics and store them in the database + 6. Return the monitoring information for the specified slice + + Args: + slice_id (str): The identifier of the network slice to monitor + slo_sle_template (dict): The SLO-SLE template for the network slice + sdps (list): The SDPs for the network slice + + Returns: + dict: Contains monitoring information for the specified slice + """ + payload = { + "slice_id": slice_id, + "slo_sle_template": slo_sle_template, + "sdps": sdps + } + + # Request the realizer to retrieve the metrics for the links of the specified slice + await realizer(payload, action="MONITOR", controller_type=self.controller_type) + + # Request the mapper to aggregate the metrics and store them in the database + metrics = mapper(payload, action="MONITOR") + + return metrics \ No newline at end of file diff --git a/src/mapper/aggregate_monitoring.py b/src/mapper/aggregate_monitoring.py new file mode 100644 index 0000000..91d3c0f --- /dev/null +++ b/src/mapper/aggregate_monitoring.py @@ -0,0 +1,64 @@ +import logging +import time +from flask import current_app +from src.database.telemetry_client_db import get_telemetry, upsert_telemetry +from src.utils.safe_get import safe_get + + +def aggregate_monitoring(slice_id, slo_sle_template): + """ + Aggregates monitoring data for a given slice + + Args: + slice_id (str): The identifier of the network slice. + slo_sle_template (dict): The SLO-SLE template for the network slice. + """ + + if slice_id in current_app.config["TELEMETRY_CACHE"]: + slos = safe_get(slo_sle_template, ["slo-policy", "metric-bound"]) + slo_bandwidth = [ s["bound"] for s in slos if s.get("metric-type", "") == "ietf-network-slice-service:two-way-bandwidth" ][0] if slos else 0 + slo_latency = [ s["bound"] for s in slos if s.get("metric-type", "") == "ietf-network-slice-service:two-way-delay-maximum" ][0] if slos else "N/A" + + links = current_app.config["TELEMETRY_CACHE"][slice_id] + + if not links: + raise Exception(f"No telemetry data available for slice '{slice_id}'") + + bandwidth = min([link["bandwidth"] for _, link in links.items()]) + latency = sum([link["latency"] for _, link in links.items()]) + + violated_metrics = [] + + if slo_latency != "N/A" and latency > float(slo_latency): + violated_metrics.append("latency") + + if slo_bandwidth != 0 and bandwidth < float(slo_bandwidth): + violated_metrics.append("bandwidth") + + telemetry_json = { + "latency": latency, + "bandwidth": bandwidth + } + + compliance_json = { + "is_compliant": len(violated_metrics) == 0, + "violated_metrics": violated_metrics + } + + current_timestamp = time.time() + + upsert_telemetry( + slice_id=slice_id, + telemetry_list=telemetry_json, + slo_sle_compliance=compliance_json, + timestamp=current_timestamp + ) + + return { + "slice_id": slice_id, + "telemetry_list": telemetry_json, + "slo_sle_compliance": compliance_json, + "timestamp": current_timestamp + } + else: + raise Exception(f"Telemetry cache for slice '{slice_id}' is not initialized. Please make sure the SDN controller is sending telemetry data for this slice.") \ No newline at end of file diff --git a/src/mapper/main.py b/src/mapper/main.py index 6a3b160..3c1e943 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -20,12 +20,13 @@ from src.utils.safe_get import safe_get from .slo_viability import slo_viability from .get_service_template import get_service_template from .process_connnectivity import process_connectivity +from .aggregate_monitoring import aggregate_monitoring from src.realizer.main import realizer from flask import current_app from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data from src.database.service_db import save_data, update_data -def mapper(ietf_intent, controller_type="TFS"): +def mapper(payload, controller_type="TFS", action="CREATE"): """ Map an IETF network slice intent to the most suitable Network Resource Partition (NRP). @@ -44,128 +45,137 @@ def mapper(ietf_intent, controller_type="TFS"): dict or None: Optimal path if planner is enabled; otherwise, None. """ optimal_path = None - services = [ietf_intent] - - if current_app.config["NRP_ENABLED"]: - # Retrieve NRP view - nrp_view = realizer(None, True, "READ") - - # Extract Service Level Objectives (SLOs) from the intent - slos = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] - if slos: - # Find candidate NRPs that can meet the SLO requirements - candidates = [ - (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}") - - # Select the best NRP based on candidates - best_nrp = max(candidates, key=lambda x: x[1])[0] if candidates else None - logging.debug(f"Best NRP: {best_nrp}") - - if best_nrp: - best_nrp["slices"].append(ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]) - # Update NRP view - realizer(ietf_intent, True, "UPDATE") - # TODO Here we should put how the slice is attached to an already created nrp - else: - # Request the controller to create a new NRP that meets the SLOs - answer = realizer(ietf_intent, True, "CREATE", best_nrp) - if not answer: - 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, current_app.config["PLANNER_TYPE"]) - logging.debug(f"Optimal path: {optimal_path}") - - if controller_type == "RESTCONF": - # Initialize available templates - templates = get_data_store("/ietf-network-slice-service:network-slice-services/slo-sle-templates/slo-sle-template") - normalized_templates = normalize_libyang_data(templates) - logging.debug(f"Normalized templates: {normalized_templates}") - available_templates = safe_get(normalized_templates, ["slo-sle-templates", "slo-sle-template"]) or [] + services = None + + if action == "CREATE": + ietf_intent = payload + services = [ietf_intent] + if current_app.config["NRP_ENABLED"]: + # Retrieve NRP view + nrp_view = realizer(None, True, "READ") + + # Extract Service Level Objectives (SLOs) from the intent + slos = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] + if slos: + # Find candidate NRPs that can meet the SLO requirements + candidates = [ + (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}") + + # Select the best NRP based on candidates + best_nrp = max(candidates, key=lambda x: x[1])[0] if candidates else None + logging.debug(f"Best NRP: {best_nrp}") + + if best_nrp: + best_nrp["slices"].append(ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]) + # Update NRP view + realizer(ietf_intent, True, "UPDATE") + # TODO Here we should put how the slice is attached to an already created nrp + else: + # Request the controller to create a new NRP that meets the SLOs + answer = realizer(ietf_intent, True, "CREATE", best_nrp) + if not answer: + 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, current_app.config["PLANNER_TYPE"]) + logging.debug(f"Optimal path: {optimal_path}") - # Add templates from intent - for template in safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates", "slo-sle-template"]): - available_templates.append(template) - logging.debug(f"Available templates: {available_templates}") - - services = [] - - # Process each slice service - for slice_service in safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service"]): - service_id = safe_get(slice_service, ["id"]) - logging.debug(f"Service ID: {service_id}") - - way = safe_get(slice_service, ['service-tags', 'tag-type', 0, 'tag-type-value', 0]) - logging.debug(f"Way: {way}") + if controller_type == "RESTCONF": + # Initialize available templates + templates = get_data_store("/ietf-network-slice-service:network-slice-services/slo-sle-templates/slo-sle-template") + normalized_templates = normalize_libyang_data(templates) + logging.debug(f"Normalized templates: {normalized_templates}") + available_templates = safe_get(normalized_templates, ["slo-sle-templates", "slo-sle-template"]) or [] - # Get service-level template - service_template = get_service_template(slice_service, available_templates) - logging.debug(f"Service Template: {service_template}") + # Add templates from intent + for template in safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services","slo-sle-templates", "slo-sle-template"]): + available_templates.append(template) + logging.debug(f"Available templates: {available_templates}") - # Process connection groups - for connection_group in safe_get(slice_service, ["connection-groups", "connection-group"]): - connection_group_id = safe_get(connection_group, ['id']) - group_id = f"{service_id}-{connection_group_id}" - logging.debug(f"Group ID: {group_id}") - - # Start with service-level template for this group - template = service_template # Reset template for each connection group - - # Override template if specified at group level - group_template = get_service_template(connection_group, available_templates) - if group_template is not None: - template = group_template - logging.debug(f"Group Template: {template}") + services = [] + + # Process each slice service + for slice_service in safe_get(ietf_intent, ["ietf-network-slice-service:network-slice-services", "slice-service"]): + service_id = safe_get(slice_service, ["id"]) + logging.debug(f"Service ID: {service_id}") + + way = safe_get(slice_service, ['service-tags', 'tag-type', 0, 'tag-type-value', 0]) + logging.debug(f"Way: {way}") - connectivity_type = safe_get(connection_group, ['connectivity-type']) - logging.debug(f"Connectivity Type: {connectivity_type}") + # Get service-level template + service_template = get_service_template(slice_service, available_templates) + logging.debug(f"Service Template: {service_template}") - # Process connectivity constructs - for connectivity_construct in safe_get(connection_group, ["connectivity-construct"]): - connectivity_construct_id = safe_get(connectivity_construct, ['id']) - construct_id = f"{group_id}-{connectivity_construct_id}" - logging.debug(f"Construct ID: {construct_id}") + # Process connection groups + for connection_group in safe_get(slice_service, ["connection-groups", "connection-group"]): + connection_group_id = safe_get(connection_group, ['id']) + group_id = f"{service_id}-{connection_group_id}" + logging.debug(f"Group ID: {group_id}") + + # Start with service-level template for this group + template = service_template # Reset template for each connection group - # Start with group-level template for this construct - final_template = template # Reset template for each connectivity construct + # Override template if specified at group level + group_template = get_service_template(connection_group, available_templates) + if group_template is not None: + template = group_template + logging.debug(f"Group Template: {template}") - # Override template if specified at construct level - construct_template = get_service_template(connectivity_construct, available_templates) - if construct_template is not None: - final_template = construct_template - logging.debug(f"Final Template: {final_template}") + connectivity_type = safe_get(connection_group, ['connectivity-type']) + logging.debug(f"Connectivity Type: {connectivity_type}") - # Process SDPs based on connectivity type - sdps = process_connectivity( - connection_group_id, - connectivity_type, - connectivity_construct, - connectivity_construct_id, - slice_service - ) - logging.debug(f"SDPs: {sdps}") - if sdps: # Only append if SDPs were found - service = { - "id": construct_id, - "template": final_template, - "connectivity_type": connectivity_type, - "sdps": sdps, - "way": way - } - services.append(service) - if not current_app.config["DUMMY_MODE"]: - # Save mapping from service_id to slice_id - save_data(service_id=safe_get(service, ["id"]), slice_id=service_id) - logging.debug(f"Service added: {service}") + # Process connectivity constructs + for connectivity_construct in safe_get(connection_group, ["connectivity-construct"]): + connectivity_construct_id = safe_get(connectivity_construct, ['id']) + construct_id = f"{group_id}-{connectivity_construct_id}" + logging.debug(f"Construct ID: {construct_id}") + + # Start with group-level template for this construct + final_template = template # Reset template for each connectivity construct + + # Override template if specified at construct level + construct_template = get_service_template(connectivity_construct, available_templates) + if construct_template is not None: + final_template = construct_template + logging.debug(f"Final Template: {final_template}") - # Break only for point-to-point - if connectivity_type == "point-to-point": - break + # Process SDPs based on connectivity type + sdps = process_connectivity( + connection_group_id, + connectivity_type, + connectivity_construct, + connectivity_construct_id, + slice_service + ) + logging.debug(f"SDPs: {sdps}") + if sdps: # Only append if SDPs were found + service = { + "id": construct_id, + "template": final_template, + "connectivity_type": connectivity_type, + "sdps": sdps, + "way": way + } + services.append(service) + if not current_app.config["DUMMY_MODE"]: + # Save mapping from service_id to slice_id + save_data(service_id=safe_get(service, ["id"]), slice_id=service_id) + logging.debug(f"Service added: {service}") + + # Break only for point-to-point + if connectivity_type == "point-to-point": + break + elif action == "MONITOR": + logging.debug("Mapper action: MONITOR") + slice_id = payload.get("slice_id", None) + slo_sle_template = payload.get("slo_sle_template", None) + metrics = aggregate_monitoring(slice_id, slo_sle_template) + return metrics return services, optimal_path \ No newline at end of file diff --git a/src/planner/shortest_path.py b/src/planner/shortest_path.py new file mode 100644 index 0000000..1784bf1 --- /dev/null +++ b/src/planner/shortest_path.py @@ -0,0 +1,93 @@ +# Copyright 2022-2026 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, traceback +from collections import deque +from typing import Dict, Tuple + +import logging, traceback +from collections import deque +from typing import Dict, Tuple + +def normalize_node_id(node_id: str) -> str: + if isinstance(node_id, str) and node_id.startswith("urn:tfs:node:"): + return node_id.split(":")[-1] + return node_id + +def get_shortest_path(network, src_node_id, dst_node_id, directed_graph: bool = False) -> Tuple[Dict[str, any], int]: + try: + source_node_idx = normalize_node_id(src_node_id) + destination_node_idx = normalize_node_id(dst_node_id) + + nodes = network["node"] + links = network["ietf-network-topology:link"] + + # Construir grafo + graph = { + normalize_node_id(node["node-id"]): set() + for node in nodes + } + + for link in links: + src = normalize_node_id(link["source"]["source-node"]) + dst = normalize_node_id(link["destination"]["dest-node"]) + + graph[src].add(dst) + if not directed_graph: + graph[dst].add(src) + + # BFS + visited = {node: False for node in graph} + prev = {node: None for node in graph} + + if source_node_idx not in graph: + return {"message": f"Source node '{source_node_idx}' not found"}, 404 + + if destination_node_idx not in graph: + return {"message": f"Destination node '{destination_node_idx}' not found"}, 404 + + queue = deque([source_node_idx]) + visited[source_node_idx] = True + + while queue: + current = queue.popleft() + + if current == destination_node_idx: + break + + for neighbor in graph[current]: + if not visited[neighbor]: + visited[neighbor] = True + prev[neighbor] = current + queue.append(neighbor) + + if not visited[destination_node_idx]: + return {"message": "No path found"}, 404 + + # Reconstruir camino + path = [] + at = destination_node_idx + + while at is not None: + path.append(at) + at = prev[at] + + path.reverse() + return path, 200 + + except Exception as e: + logging.exception("Error calculating shortest path") + raise \ No newline at end of file diff --git a/src/realizer/get_metrics.py b/src/realizer/get_metrics.py new file mode 100644 index 0000000..6f02e79 --- /dev/null +++ b/src/realizer/get_metrics.py @@ -0,0 +1,35 @@ +import logging +import asyncio +from .restconf.connectors.tfs_connector import tfs_connector +from flask import current_app + +async def get_metrics(path, slice_id, controller_type): + if controller_type == "RESTCONF": + links = [] + if slice_id not in current_app.config["TELEMETRY_CACHE"]: + logging.debug(f"Telemetry for slice '{slice_id}' not in cache. Initializing.") + current_app.config["TELEMETRY_CACHE"][slice_id] = {} + for i in range(len(path)-1): + link_id = f"{path[i]}-{path[i+1]}" + links.append(link_id) + current_app.config["TELEMETRY_CACHE"][slice_id][link_id] = { + "timestamp": None, #time.time(), + "bandwidth": 0, + "latency": 0, + "services": [] + } + # Get telemetry only of the links in the shortest path: + logging.debug(f"Starting telemetry streams for slice '{slice_id}'") + connector = tfs_connector() + bg_loop = connector.get_background_loop() + coro = connector.startStreams( + current_app.config["RESTCONF_IP"], + current_app.config["SDN_SUBSCRIPTION_PERIOD"], + links, + slice_id, + current_app.config["TELEMETRY_CACHE"] + ) + future = asyncio.run_coroutine_threadsafe(coro, bg_loop) + await asyncio.wrap_future(future) + else: + logging.debug(f"Telemetry for slice '{slice_id}' already in cache. Skipping.") \ No newline at end of file diff --git a/src/realizer/main.py b/src/realizer/main.py index 393d025..63e0363 100644 --- a/src/realizer/main.py +++ b/src/realizer/main.py @@ -17,9 +17,13 @@ import logging 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.realizer.restconf.connectors.tfs_connector import tfs_connector +from flask import current_app -def realizer(service, need_nrp=False, order=None, nrp=None, controller_type=None, response=None, rules = None): +async def realizer(payload, need_nrp=False, order=None, nrp=None, controller_type=None, response=None, rules = None, action="CREATE"): """ Manage the slice creation workflow. @@ -39,45 +43,67 @@ def realizer(service, need_nrp=False, order=None, nrp=None, controller_type=None Returns: dict: A realization request for the specified network slice type. """ - if need_nrp: - # Perform NRP-related operations - nrp_view = nrp_handler(order, nrp) - return nrp_view - else: - # Select slice service method - 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 [] + if action == "CREATE": + service = payload + if need_nrp: + # Perform NRP-related operations + nrp_view = nrp_handler(order, nrp) + return nrp_view + else: + # Select slice service method + 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) + 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) + 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" + 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" + 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 = service.get("way", None) or safe_get(service, ['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=service, response=response, rules = rules) + return request + 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) + 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}") + await get_metrics(path, slice_id, controller_type) else: - logging.warning("Cannot determine the realization way from rules. Skipping request.") - return None - way = selected_way + raise Exception("Error: Shortest path not retrieved ") else: - way = service.get("way", None) or safe_get(service, ['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=service, response=response, rules = rules) - return request + raise Exception("Error: Topology not retrieved") \ No newline at end of file diff --git a/src/realizer/restconf/connectors/tfs_connector.py b/src/realizer/restconf/connectors/tfs_connector.py index a4f2139..863c02c 100644 --- a/src/realizer/restconf/connectors/tfs_connector.py +++ b/src/realizer/restconf/connectors/tfs_connector.py @@ -14,13 +14,38 @@ # This file includes original contributions from Telefonica Innovación Digital S.L. -import logging, requests, json -from src.config.constants import NBI_L2_PATH, NBI_L3_PATH +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 + +# Temp until moving to .env +SDN_SUBSCRIPTION_PERIOD = 10 # seconds +SDN_SUBS_INACTIVITY_THRESHOLD = 30 # seconds class tfs_connector(): """ Helper class to interact with TeraFlowSDN Northbound Interface (NBI) and WebUI. """ + _loop = None + _thread = None + _session = None + + @classmethod + def get_background_loop(cls): + if cls._loop is None: + cls._loop = asyncio.new_event_loop() + cls._thread = threading.Thread(target=cls._run_loop, args=(cls._loop,), daemon=True) + cls._thread.start() + return cls._loop + + @staticmethod + def _run_loop(loop): + asyncio.set_event_loop(loop) + loop.run_forever() + + def __init__(self): + pass + def webui_post(self, tfs_ip, service): """ Post service descriptor to TFS WebUI. @@ -100,4 +125,141 @@ class tfs_connector(): response.raise_for_status() logging.debug('Service deleted successfully') logging.debug("Http response: %s",response.text) - return response \ No newline at end of file + return response + + def get_network_topology(self, tfs_ip: str, slice_id: str) -> Tuple[Dict[str, any], int]: + user="admin" + password="admin" + url = f'http://{user}:{password}@{tfs_ip}' + url = url + f'/{NBI_IETF_NETWORKS_PATH}' + # MOCKED TOPOLOGY + response = requests.get(url, timeout=60) + response.raise_for_status() + network_raw = response.json() + network = next((n["ietf-network:networks"]["network"][0] for n in network_raw if n["ietf-network:networks"]["network"][0]["network-id"] == "urn:tfs:network:admin"), None) + logging.debug(f"Retrieved topology for slice '{slice_id}': {network}") + + return network, 200 + + # --- SDN STREAMS --- + + async def get_session(self): + # Crea la sesión si no existe en el loop de background + if tfs_connector._session is None or tfs_connector._session.closed: + tfs_connector._session = aiohttp.ClientSession() + return tfs_connector._session + + async def close_session(self): + # Llama a esto al apagar tu app + if tfs_connector._session: + await tfs_connector._session.close() + + def _extract_stream_uri(self, payload, base_url): + if not isinstance(payload, dict): + return None + + if "uri" in payload and isinstance(payload["uri"], str): + uri = payload["uri"].strip() + if uri.startswith(("http://", "https://")): + return uri + if base_url: + return f"{base_url.rstrip('/')}/{uri.lstrip('/')}" + return uri + + for key in ("ietf-subscribed-notifications:subscription-result", "subscription-result", "stream", "ietf-subscribed-notifications:stream"): + if key in payload: + uri = self._extract_stream_uri(payload[key], base_url) + if uri: + return uri + + for value in payload.values(): + if isinstance(value, dict): + uri = self._extract_stream_uri(value, base_url) + if uri: + return uri + + return None + + async def listenStream(self, session, link_id, slice_id, stream_url, telemetry_cache): + logging.debug(f"Listening to stream for link {link_id} in slice {slice_id} at {stream_url}") + try: + async with session.get( + stream_url, + auth=aiohttp.BasicAuth("admin", "admin"), + timeout=None, + headers = { + "Accept": "text/event-stream", + "Cache-Control": "no-cache" + } + ) as resp: + logging.debug(f"Connection established for link {link_id}") + buffer = "" + + async for chunk in resp.content.iter_any(): + buffer += chunk.decode() + + while "\n\n" in buffer: + event_block, buffer = buffer.split("\n\n", 1) + + for line in event_block.split("\n"): + if line.startswith("data:"): + json_str = line.replace("data:", "").strip() + + try: + data = json.loads(json_str) + logging.debug(f"Received data for link {link_id}: {data}") + + telemetry = data["notification"]["push-update"]["datastore-contents"]["simap-telemetry:simap-telemetry"] + bw = telemetry.get("bandwidth-utilization", "N/A") + latency = telemetry.get("latency", "N/A") + telemetry_cache.setdefault(slice_id, {})[link_id] = { + "timestamp": data["notification"]["eventTime"], + "bandwidth": float(bw) if bw != "N/A" else bw, + "latency": float(latency) if latency != "N/A" else latency, + "services": telemetry.get("related-service-ids", []) + } + except json.JSONDecodeError: + logging.error(f"Error parsing JSON for link {link_id}: {json_str}") + except Exception as e: + logging.error(f"Error processing data for link {link_id}: {e}") + except aiohttp.ClientError as e: + logging.error(f"Connection error with stream for link {link_id}: {e}") + except Exception as e: + logging.error(f"Unexpected error in listenStream for link {link_id}: {e}") + + logging.info(f"Stopped listening to stream for link {link_id} in slice {slice_id}") + + async def startStreams(self, tfs_ip, suscription_period, links, slice_id, telemetry_cache): + #async with self.get_session() as session: #timeout = aiohttp.ClientTimeout(total=10) + session = await self.get_session() + tasks = [] + base_url = f'http://{tfs_ip}' + url = f'{base_url}{NBI_SIMAP_SUSCRIPTION_PATH}' + + for link in links: + sub_resp = await session.post(url, json={ + "ietf-subscribed-notifications:input": { + "datastore": "operational", + "ietf-yang-push:datastore-xpath-filter": f"/ietf-network:networks/network=admin/ietf-network-topology:link={link}/simap-telemetry:simap-telemetry", + "ietf-yang-push:periodic": { + "ietf-yang-push:period": suscription_period + } + } + }, + auth=aiohttp.BasicAuth("admin", "admin") + ) + sub_resp.raise_for_status() + sub_data = await sub_resp.json(content_type=None) + stream_url = self._extract_stream_uri(sub_data, base_url) + + if stream_url: + tasks.append( + asyncio.create_task( + self.listenStream(session, link, slice_id, stream_url, telemetry_cache) + ) + ) + logging.debug("Created telemetry listener for link '%s' in slice '%s' at %s", link, slice_id, stream_url) + else: + logging.warning("Subscription for link '%s' did not return a stream URI. Response: %s", link, sub_data) + + logging.debug(f"Telemetry cache for slice '{slice_id}': {telemetry_cache.get(slice_id)}") diff --git a/src/tests/collections/NSC.postman_collection.json b/src/tests/collections/NSC.postman_collection.json index 08f9531..568e796 100644 --- a/src/tests/collections/NSC.postman_collection.json +++ b/src/tests/collections/NSC.postman_collection.json @@ -1,10 +1,8 @@ { "info": { - "_postman_id": "43660461-a738-488b-a6f5-2c244a9cae25", + "_postman_id": "420789ec-9203-4f33-9511-2100c7a934f0", "name": "NSC", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "45361142", - "_collection_link": "https://go.postman.co/collection/45361142-43660461-a738-488b-a6f5-2c244a9cae25?source=collection_link" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { @@ -129,7 +127,7 @@ "name": "Slices", "item": [ { - "name": "Slice Telemetry by client", + "name": "Create Slice Subscription", "request": { "auth": { "type": "basic", @@ -146,28 +144,35 @@ } ] }, - "method": "GET", + "method": "POST", "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"slice_id\": \"{{SLICE_ID}}\",\n \"frequency\": 10\n}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}", "host": [ "{{HOST}}" ], "path": [ "restconf", - "telemetry", "operations", - "subscriptions", - "{{CLIENT_ID}}", - "slices", - "{{SLICE_ID}}" + "telemetry", + "subscription", + "{{CLIENT_ID}}" ] } }, "response": [] }, { - "name": "Slice Telemetry by client", + "name": "Get Slice Subscription", "request": { "auth": { "type": "basic", @@ -184,20 +189,20 @@ } ] }, - "method": "DELETE", + "method": "GET", "header": [], "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}/slice/{{SLICE_ID}}", "host": [ "{{HOST}}" ], "path": [ "restconf", - "telemetry", "operations", - "subscriptions", + "telemetry", + "subscription", "{{CLIENT_ID}}", - "slices", + "slice", "{{SLICE_ID}}" ] } @@ -205,7 +210,7 @@ "response": [] }, { - "name": "Slice Telemetry by client", + "name": "Update Slice Subscription", "request": { "auth": { "type": "basic", @@ -222,11 +227,11 @@ } ] }, - "method": "POST", + "method": "PUT", "header": [], "body": { "mode": "raw", - "raw": "{\n \"frequency\": 2\n}", + "raw": "{\n \"frequency\": 5\n}", "options": { "raw": { "language": "json" @@ -234,22 +239,99 @@ } }, "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}/slices/{{SLICE_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}/slice/{{SLICE_ID}}", "host": [ "{{HOST}}" ], "path": [ "restconf", + "operations", "telemetry", + "subscription", + "{{CLIENT_ID}}", + "slice", + "{{SLICE_ID}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Slice Subscription", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}/slice/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", "operations", - "subscriptions", + "telemetry", + "subscription", "{{CLIENT_ID}}", - "slices", + "slice", "{{SLICE_ID}}" ] } }, "response": [] + }, + { + "name": "Stream Telemetry Subscription", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}/slice/{{SLICE_ID}}/stream", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "operations", + "telemetry", + "subscription", + "{{CLIENT_ID}}", + "slice", + "{{SLICE_ID}}", + "stream" + ] + } + }, + "response": [] } ] }, @@ -271,18 +353,18 @@ } ] }, - "method": "DELETE", + "method": "GET", "header": [], "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}", "host": [ "{{HOST}}" ], "path": [ "restconf", - "telemetry", "operations", - "subscriptions", + "telemetry", + "subscription", "{{CLIENT_ID}}" ] } @@ -290,7 +372,7 @@ "response": [] }, { - "name": "All subscriptions of a client", + "name": "Delete all subscriptions of a client", "request": { "auth": { "type": "basic", @@ -307,18 +389,18 @@ } ] }, - "method": "GET", + "method": "DELETE", "header": [], "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/subscriptions/{{CLIENT_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/subscription/{{CLIENT_ID}}", "host": [ "{{HOST}}" ], "path": [ "restconf", - "telemetry", "operations", - "subscriptions", + "telemetry", + "subscription", "{{CLIENT_ID}}" ] } @@ -326,90 +408,85 @@ "response": [] } ] - }, + } + ] + }, + { + "name": "slices", + "item": [ { - "name": "Stream", - "item": [ - { - "name": "Stream All Telemetry Subscriptions by client", - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin", - "type": "string" - } - ] + "name": "Create Slice", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" }, - "method": "GET", - "header": [], - "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}", - "host": [ - "{{HOST}}" - ], - "path": [ - "restconf", - "telemetry", - "operations", - "stream", - "{{CLIENT_ID}}" - ] + { + "key": "username", + "value": "admin", + "type": "string" } - }, - "response": [] + ] }, - { - "name": "Stream Telemetry Subscription by client Copy", - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin", - "type": "string" - } - ] + "method": "POST", + "header": [], + "body": {}, + "url": { + "raw": "{{HOST}}/restconf/data/ietf-network-slice-service:network-slice-services", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "data", + "ietf-network-slice-service:network-slice-services" + ] + } + }, + "response": [] + }, + { + "name": "Get Slices", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" }, - "method": "GET", - "header": [], - "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/stream/{{CLIENT_ID}}/slices/{{SLICE_ID}}", - "host": [ - "{{HOST}}" - ], - "path": [ - "restconf", - "telemetry", - "operations", - "stream", - "{{CLIENT_ID}}", - "slices", - "{{SLICE_ID}}" - ] + { + "key": "username", + "value": "admin", + "type": "string" } - }, - "response": [] + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/data/ietf-network-slice-service:network-slice-services", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", + "data", + "ietf-network-slice-service:network-slice-services" + ] } - ] + }, + "response": [] } ] }, { - "name": "Telemetry of a Slice", + "name": "Telemetry of All Slices", "request": { "auth": { "type": "basic", @@ -429,15 +506,50 @@ "method": "GET", "header": [], "url": { - "raw": "{{HOST}}/restconf/telemetry/operations/slices/{{SLICE_ID}}", + "raw": "{{HOST}}/restconf/operations/telemetry/slice", "host": [ "{{HOST}}" ], "path": [ "restconf", + "operations", "telemetry", + "slice" + ] + } + }, + "response": [] + }, + { + "name": "Telemetry of a Specific Slice", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/restconf/operations/telemetry/slice/{{SLICE_ID}}", + "host": [ + "{{HOST}}" + ], + "path": [ + "restconf", "operations", - "slices", + "telemetry", + "slice", "{{SLICE_ID}}" ] } diff --git a/src/tests/collections/NSC.postman_environment.json b/src/tests/collections/NSC.postman_environment.json index 19c9e3b..34117f0 100644 --- a/src/tests/collections/NSC.postman_environment.json +++ b/src/tests/collections/NSC.postman_environment.json @@ -4,19 +4,19 @@ "values": [ { "key": "HOST", - "value": "http://172.17.0.2:8086/nsc", + "value": "http://192.168.27.189:8085/nsc", "type": "default", "enabled": true }, { "key": "CLIENT_ID", - "value": "", + "value": "test-client", "type": "default", "enabled": true }, { "key": "SLICE_ID", - "value": "", + "value": "simap-slice", "type": "default", "enabled": true } diff --git a/swagger/restconf_namespace.py b/swagger/restconf_namespace.py index aa7e18a..079faf0 100644 --- a/swagger/restconf_namespace.py +++ b/swagger/restconf_namespace.py @@ -15,12 +15,8 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. import logging -import time -import json -import uuid - from flask import request, Response, request, current_app, stream_with_context -from flask_restx import Namespace, Resource, fields, reqparse +from flask_restx import Namespace, Resource, fields from src.main import NSController from src.api.main import Api from swagger.models.create_models_restconf import create_ietf_network_slice_nbi_yang_model @@ -182,7 +178,6 @@ class SdpList(Resource): @restconf_ns.doc(summary="Create Slice Service SDPs") @restconf_ns.expect(sdp_model) - #@restconf_ns.expect(sdp_model) @restconf_ns.response(201, "SDP created", slice_response_model) @restconf_ns.response(409, "SDP already exists") def post(self, slice_service_id): @@ -219,142 +214,181 @@ class Sdp(Resource): return Api(controller).delete_sdps(slice_service_id, sdp_id) # ================================================================================================================================================ -@restconf_ns.route("/telemetry/operations/clients") -class TelemetryAllClientsRequest(Resource): +@restconf_ns.route("/operations/telemetry/client") +class ClientsList(Resource): @restconf_ns.doc(description="Gets the list of all registered clients") @restconf_ns.response(200, "Clients retrieved") + @restconf_ns.response(404, "No clients found") + @restconf_ns.response(500, "Internal server error") def get(self): - clients, code = Api(NSController(controller_type="RESTCONF")).getAllClients() - return [clientName for clientName, clientData in clients.items()], code - -@restconf_ns.route("/telemetry/operations/clients/") -class TelemetryClientRequest(Resource): + return Api(NSController(controller_type="RESTCONF")).get_clients() + + @restconf_ns.doc(description="Deletes all clients") + @restconf_ns.response(204, "Clients deleted") + @restconf_ns.response(500, "Internal server error") + def delete(self): + return Api(NSController(controller_type="RESTCONF")).delete_clients() + +@restconf_ns.route("/operations/telemetry/client/") +class Clients(Resource): + @restconf_ns.response(200, "Client retrieved") + @restconf_ns.response(404, "No client found") + @restconf_ns.response(500, "Internal server error") + def get(self, client_id): + return Api(NSController(controller_type="RESTCONF")).get_clients(client_id) + @restconf_ns.doc(description="Creates a new client") - @restconf_ns.response(200, "Client created") - def post(self, clientId): return Api(NSController(controller_type="RESTCONF")).createClient(clientId) + @restconf_ns.response(201, "Client created", model=restconf_ns.model("ClientModel", { + "client_id": fields.String(required=True, description="Client ID"), + })) + @restconf_ns.response(500, "Internal server error") + def post(self, client_id): + return Api(NSController(controller_type="RESTCONF")).add_client(client_id) @restconf_ns.doc(description="Deletes a given client") - @restconf_ns.response(200, "Client deleted") - def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteClient(clientId) - + @restconf_ns.response(204, "Client deleted") + @restconf_ns.response(404, "No client found") + @restconf_ns.response(500, "Internal server error") + def delete(self, client_id): + return Api(NSController(controller_type="RESTCONF")).delete_clients(client_id) -@restconf_ns.route("/telemetry/operations/slices") -class TelemetrySliceRequest(Resource): - @restconf_ns.doc(description="Gets latest telemetry of a certain network slice, or of all slices.", - params={ - "sliceId": { - "description": "Network Slice ID (optional). If not provided, retrieves all slices.", - "in": "query", - "required": False, - "type": "string" - } - } +@restconf_ns.route("/operations/telemetry/subscription/") +class Subscriptions(Resource): + @restconf_ns.doc( + description="Retrieves all telemetry subscriptions of a given client.", + params={"client_id": "Client ID."} ) - @restconf_ns.response(200, "Telemetry retrieved") - @restconf_ns.response(404, "Slice not found") - def get(self): + @restconf_ns.response(200, "Subscriptions successfully retrieved") + @restconf_ns.response(500, "Internal server error") + def get(self, client_id): api = Api(NSController(controller_type="RESTCONF")) - sliceId = request.args.get("sliceId", None) - - ret = {} - if sliceId: ret = current_app.ensure_sync(api.getTelemetry)(sliceId) - else: ret = current_app.ensure_sync(api.getAllTelemetry)() - - logging.info(f"Telemetry retrieved successfully for sliceId={sliceId if sliceId else 'ALL'}: {ret}") - return ret, 200 - -@restconf_ns.route("/telemetry/operations/stream/") -class TelemetryStreamRequest(Resource): + return current_app.ensure_sync(api.get_subscriptions)(client_id) + @restconf_ns.doc( - description="Opens a telemetry stream of a given subscription (or all of them) of a certain client", - params={ - "sliceId": { - "description": "Network Slice ID (optional). If not provided, streams all subscriptions.", - "in": "query", - "required": False, - "type": "string" - } - } + description="Creates a new telemetry subscription for a given client.", + params={"client_id": "Client ID."} ) - @restconf_ns.response(201, "Telemetry opened") + @restconf_ns.response(201, "Subscription successfully created") @restconf_ns.response(400, "Invalid request") - def get(self, clientId): + @restconf_ns.response(500, "Internal server error") + @restconf_ns.expect(restconf_ns.model("TelemetrySubscriptionModel", { + "slice_id": fields.String(required=True, description="Network Slice ID"), + "frequency": fields.Integer(required=True, description="Telemetry push frequency in seconds") + }), validate=True) + def post(self, client_id): + data = request.json + slice_id = data.get("slice_id") + frequency = data.get("frequency") + api = Api(NSController(controller_type="RESTCONF")) - return Response( - stream_with_context( - api.sync_stream( - api.openSubscriptions, - clientId, - request.args.get("sliceId", None) - ) - ), - mimetype="text/event-stream" + return current_app.ensure_sync(api.add_subscription)( + client_id, + slice_id, + frequency ) + + @restconf_ns.doc( + description="Deletes all telemetry subscriptions of a given client.", + params={"client_id": "Client ID."} + ) + @restconf_ns.response(204, "Subscriptions successfully deleted") + @restconf_ns.response(500, "Internal server error") + def delete(self, client_id): + return Api(NSController(controller_type="RESTCONF")).delete_subscriptions(client_id) -@restconf_ns.route("/telemetry/operations/slice-subscriptions/") -class TelemetrySubscriptionsRequest(Resource): +@restconf_ns.route("/operations/telemetry/subscription//slice/") +class SliceSubscriptions(Resource): @restconf_ns.doc( - description="Retrieves a specific telemetry subscription (or all of them) of a given client.", + description="Retrieves a specific telemetry subscription of a given client.", params={ - "sliceId": { - "description": "Network Slice ID (optional). If not provided, retrieves all subscriptions.", - "in": "query", - "required": False, - "type": "string" - } + "client_id": "Client ID.", + "slice_id": "Network Slice ID." } ) - @restconf_ns.response(201, "Subscription(s) successfully retrieved") - @restconf_ns.response(400, "Invalid request") - def get(self, clientId): + @restconf_ns.response(200, "Subscription successfully retrieved") + @restconf_ns.response(500, "Internal server error") + def get(self, client_id, slice_id): api = Api(NSController(controller_type="RESTCONF")) - return current_app.ensure_sync(api.getTelemetrySubscriptions)(clientId, request.args.get("sliceId", None), False) + return current_app.ensure_sync(api.get_subscriptions)(client_id, slice_id) @restconf_ns.doc( - description="Deletes a specific telemetry subscription (or all of them) of a given client.", + description="Updates a telemetry subscription for a given client.", params={ - "sliceId": { - "description": "Network Slice ID (optional). If not provided, deletes all subscriptions.", - "in": "query", - "required": False, - "type": "string" - } + "client_id": "Client ID.", + "slice_id": "Network Slice ID." } ) - @restconf_ns.response(201, "Subscription(s) successfully deleted") + @restconf_ns.response(201, "Subscription successfully updated") @restconf_ns.response(400, "Invalid request") - def delete(self, clientId): return Api(NSController(controller_type="RESTCONF")).deleteTelemetrySubscriptions(clientId, request.args.get("sliceId", None)) - + @restconf_ns.response(500, "Internal server error") + @restconf_ns.expect(restconf_ns.model("TelemetryUpdateSubscriptionModel", { + "frequency": fields.Integer(required=True, description="Telemetry push frequency in seconds") + }), validate=True) + def put(self, client_id, slice_id): + data = request.json + frequency = data.get("frequency") + + api = Api(NSController(controller_type="RESTCONF")) + return current_app.ensure_sync(api.update_subscription)( + client_id, + slice_id, + frequency + ) + @restconf_ns.doc( - description="Creates a new telemetry subscription for a given client.", + description="Deletes a specific telemetry subscription of a given client.", params={ - "sliceId": { - "description": "Network Slice ID. If not provided, the sliceId must be included in the request body.", - "in": "query", - "required": False, - "type": "string" - } + "client_id": "Client ID.", + "slice_id": "Network Slice ID." } ) - @restconf_ns.response(201, "Subscription successfully created") - @restconf_ns.response(400, "Invalid request") - @restconf_ns.expect(restconf_ns.model("TelemetrySubscriptionModel", { - "sliceId": fields.String(required=False, description="Network Slice ID (if not provided as query param)"), - "frequency": fields.Integer(required=True, description="Telemetry push frequency in seconds"), - "src": fields.String(required=True, description="Source Node Id"), - "dst": fields.String(required=True, description="Destination Node Id") - }), validate=True) - def post(self, clientId): - sliceId = request.args.get("sliceId", None) - if not sliceId: sliceId = request.json.get("sliceId", None) - if not sliceId: return {"error": "sliceId query param is required"}, 400 + @restconf_ns.response(204, "Subscription successfully deleted") + @restconf_ns.response(500, "Internal server error") + def delete(self, client_id, slice_id): + return Api(NSController(controller_type="RESTCONF")).delete_subscriptions(client_id, slice_id) + +@restconf_ns.route("/operations/telemetry/subscription//slice//stream") +class SliceSubscriptionsStream(Resource): + @restconf_ns.doc( + description="Opens a telemetry stream of a given subscription of a certain client", + params={ + "client_id": "Client ID.", + "slice_id": "Network Slice ID." + } + ) + @restconf_ns.response(200, "Telemetry stream opened") + @restconf_ns.response(500, "Internal server error") + def get(self, client_id, slice_id): api = Api(NSController(controller_type="RESTCONF")) - return current_app.ensure_sync(api.createTelemetrySubscription)( - clientId, - sliceId, - request.json.get("frequency", None), - request.json.get("src", None), - request.json.get("dst", None), - ) \ No newline at end of file + return Response( + stream_with_context( + api.sync_stream( + api.stream_slice_subscription, + client_id, + slice_id + ) + ), + mimetype="text/event-stream" + ) + +@restconf_ns.route("/operations/telemetry/slice") +class TelemetryList(Resource): + @restconf_ns.doc(description="Gets latest telemetry of all slices.") + @restconf_ns.response(200, "Telemetry retrieved") + @restconf_ns.response(500, "Internal server error") + def get(self): + controller = NSController(controller_type="RESTCONF") + logging.info("Retrieving latest telemetry for all slices") + return current_app.ensure_sync(Api(controller).get_telemetry)() +@restconf_ns.route("/operations/telemetry/slice/") +class Telemetry(Resource): + @restconf_ns.doc(description="Gets latest telemetry of a certain network slice") + @restconf_ns.response(200, "Telemetry retrieved") + @restconf_ns.response(404, "Slice not found") + @restconf_ns.response(500, "Internal server error") + def get(self, slice_id): + controller = NSController(controller_type="RESTCONF") + logging.info(f"Retrieving latest telemetry for slice '{slice_id}'") + return current_app.ensure_sync(Api(controller).get_telemetry)(slice_id=slice_id) \ No newline at end of file -- GitLab From c076157e241cf18a61ace25a64f042578a2d4550 Mon Sep 17 00:00:00 2001 From: velazquez Date: Mon, 6 Jul 2026 15:00:10 +0200 Subject: [PATCH 27/27] Fix the use of async functions --- src/api/main.py | 20 ++++++++++---------- src/main.py | 4 ++-- src/realizer/get_metrics.py | 4 ++-- src/realizer/main.py | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/api/main.py b/src/api/main.py index c778aa6..0685911 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -799,7 +799,7 @@ class Api: # --- SUBSCRIPTIONS --- - async def get_subscriptions(self, client_id, slice_id = None): + def get_subscriptions(self, client_id, slice_id = None): try: if slice_id is not None: try: @@ -809,7 +809,7 @@ class Api: if not subscription: raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'") - telemetry = await self.get_telemetry(slice_id) + telemetry = self.get_telemetry(slice_id) return { **subscription, @@ -823,7 +823,7 @@ class Api: for sub in subscriptions: slice_id = sub["slice_id"] - telemetry = await self.get_telemetry(slice_id) + telemetry = self.get_telemetry(slice_id) result.append({ **sub, @@ -840,7 +840,7 @@ class Api: except Exception as e: return send_response(False, code=500, message=str(e)) - async def add_subscription(self, client_id, slice_id, frequency): + def add_subscription(self, client_id, slice_id, frequency): try: try: subscription = get_subscription(client_id, slice_id) @@ -869,7 +869,7 @@ class Api: except Exception as e: return send_response(False, code=500, message=str(e)) - async def update_subscription(self, client_id, slice_id, frequency): + def update_subscription(self, client_id, slice_id, frequency): try: try: subscription = get_subscription(client_id, slice_id) @@ -918,7 +918,7 @@ class Api: # --- TELEMETRY --- - async def get_telemetry(self, slice_id = None): + def get_telemetry(self, slice_id = None): logging.debug(f"Getting telemetry for slice_id: {slice_id}") try: if slice_id is not None: @@ -938,7 +938,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 = await self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) + metrics = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) return metrics, 200 telemetry_data = {} @@ -960,7 +960,7 @@ class Api: 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] = await self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) + telemetry_data[slice_id] = self.slice_service.monitoring(slice_id, slo_sle_template, slice_sdps) return telemetry_data, 200 except ValueError as e: @@ -981,7 +981,7 @@ class Api: async def stream_client_subscriptions(self, client_id): while True: try: - data, code = await self.get_subscriptions(client_id) + data, code = self.get_subscriptions(client_id) if code == 200: yield f"data: {json.dumps(data)}\n\n" subs = data.get("subscriptions", []) @@ -997,7 +997,7 @@ class Api: async def stream_slice_subscription(self, client_id, slice_id): while True: try: - data, code = await self.get_subscriptions(client_id, slice_id) + data, code = self.get_subscriptions(client_id, slice_id) if code == 200: yield f"data: {json.dumps(data)}\n\n" freq = data.get("frequency", 5) diff --git a/src/main.py b/src/main.py index 069caf4..e11ff27 100644 --- a/src/main.py +++ b/src/main.py @@ -132,7 +132,7 @@ class NSController: "setup_time": setup_time } - async def monitoring(self, slice_id, slo_sle_template, sdps): + def monitoring(self, slice_id, slo_sle_template, sdps): """ Monitor the status of a specific network slice. @@ -159,7 +159,7 @@ class NSController: } # Request the realizer to retrieve the metrics for the links of the specified slice - await realizer(payload, action="MONITOR", controller_type=self.controller_type) + realizer(payload, action="MONITOR", controller_type=self.controller_type) # Request the mapper to aggregate the metrics and store them in the database metrics = mapper(payload, action="MONITOR") diff --git a/src/realizer/get_metrics.py b/src/realizer/get_metrics.py index 6f02e79..d204217 100644 --- a/src/realizer/get_metrics.py +++ b/src/realizer/get_metrics.py @@ -3,7 +3,7 @@ import asyncio from .restconf.connectors.tfs_connector import tfs_connector from flask import current_app -async def get_metrics(path, slice_id, controller_type): +def get_metrics(path, slice_id, controller_type): if controller_type == "RESTCONF": links = [] if slice_id not in current_app.config["TELEMETRY_CACHE"]: @@ -30,6 +30,6 @@ async def get_metrics(path, slice_id, controller_type): current_app.config["TELEMETRY_CACHE"] ) future = asyncio.run_coroutine_threadsafe(coro, bg_loop) - await asyncio.wrap_future(future) + future.result() else: logging.debug(f"Telemetry for slice '{slice_id}' already in cache. Skipping.") \ No newline at end of file diff --git a/src/realizer/main.py b/src/realizer/main.py index 63e0363..9472767 100644 --- a/src/realizer/main.py +++ b/src/realizer/main.py @@ -23,7 +23,7 @@ from src.planner.shortest_path import get_shortest_path from src.realizer.restconf.connectors.tfs_connector import tfs_connector from flask import current_app -async def realizer(payload, need_nrp=False, order=None, nrp=None, controller_type=None, response=None, rules = None, action="CREATE"): +def realizer(payload, need_nrp=False, order=None, nrp=None, controller_type=None, response=None, rules = None, action="CREATE"): """ Manage the slice creation workflow. @@ -102,7 +102,7 @@ async def realizer(payload, need_nrp=False, order=None, nrp=None, controller_typ 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}") - await get_metrics(path, slice_id, controller_type) + get_metrics(path, slice_id, controller_type) else: raise Exception("Error: Shortest path not retrieved ") else: -- GitLab