Loading src/config/.env.example +1 −3 Original line number Diff line number Diff line Loading @@ -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 Loading src/config/config.py +0 −1 Original line number Diff line number Diff line Loading @@ -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 Loading src/planner/planner.py +0 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading src/planner/tfs_optical_planner/tfs_optical.pydeleted 100644 → 0 +0 −393 Original line number Diff line number Diff line # 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 Loading
src/config/.env.example +1 −3 Original line number Diff line number Diff line Loading @@ -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 Loading
src/config/config.py +0 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
src/planner/planner.py +0 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
src/planner/tfs_optical_planner/tfs_optical.pydeleted 100644 → 0 +0 −393 Original line number Diff line number Diff line # 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