Commit 9378d4fd authored by Pablo Armingol's avatar Pablo Armingol
Browse files

code clean up

parent 0c934828
Loading
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -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

+0 −1
Original line number Diff line number Diff line
@@ -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
+0 −3
Original line number Diff line number Diff line
@@ -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
+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