From 0c9348282dbe2e681e2d69f4690511baa6ab31fd Mon Sep 17 00:00:00 2001 From: armingol Date: Tue, 19 May 2026 15:20:15 +0000 Subject: [PATCH 1/2] 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 513c6db..c429b25 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 9378d4fd1fd2cd2a2d9f8da471980556cc26407c Mon Sep 17 00:00:00 2001 From: armingol Date: Wed, 20 May 2026 07:07:36 +0000 Subject: [PATCH 2/2] 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 c429b25..977a874 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