Commit 8847b1a3 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

Merge branch 'TID_ECOC25_DEMOS' of https://labs.etsi.org/rep/tfs/nsc into TID_ECOC25_DEMOS

parents faf76cc2 6c87d2df
Loading
Loading
Loading
Loading
+24 −20
Original line number Diff line number Diff line
# Copyright 2022-2025 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
from flask import Flask
from flask_restx import Api
from flask_cors import CORS
from swagger.slice_namespace import slice_ns
from src.constants import NSC_PORT

# Configuración de logging básica para fichero
logging.basicConfig(
    filename='nsc_controller.log',
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(name)s: %(message)s',
    filemode='w'
)

# Obtener logger raíz y logger Flask
logger = logging.getLogger()
flask_logger = logging.getLogger('werkzeug')  # logger que usa Flask para accesos HTTP

# Añadir handler de fichero a logger de Flask para asegurarnos que escribe en el log
file_handler = logging.FileHandler('nsc_controller.log')
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
flask_logger.addHandler(file_handler)

app = Flask(__name__)
CORS(app)

# Create API instance
api = Api(
    app,
    version="1.0",
    title="Network Slice Controller (NSC) API",
    description="API for orchestrating and realizing transport network slice requests",
    doc="/nsc"  # Swagger UI URL
    doc="/nsc"
)

# Register namespaces
api.add_namespace(slice_ns, path="/slice")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=NSC_PORT, debug=True)
    logger.info("Running  Network Slice Controller in %s:%s", "192.168.202.50", NSC_PORT)
    app.run(host="192.168.202.50", port=NSC_PORT, debug=True)
+1 −1
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ TEMPLATES_PATH = os.path.join(SRC_PATH, "templates")
# Flag to determine if configurations should be uploaded to Teraflow
TFS_UPLOAD = False
# Teraflow IP
TFS_IP = "10.95.86.58"
TFS_IP = "172.24.36.55"
# Flag to determine if additional L2VPN configuration support is
# required for deploying L2VPNs with path selection
TFS_L2VPN_SUPPORT = False
+162 −4
Original line number Diff line number Diff line
@@ -16,11 +16,11 @@
import json
import logging
import os
import uuid
import requests
from netmiko import ConnectHandler
from src.constants import DEFAULT_LOGGING_LEVEL, SRC_PATH
from src.constants import DEFAULT_LOGGING_LEVEL

# Configure logging to provide clear and informative log messages
logging.basicConfig(
    level=DEFAULT_LOGGING_LEVEL,
    format='%(levelname)s - %(message)s')
@@ -174,10 +174,168 @@ def send_network_slice_request(data: str, action: str = "create") -> dict:
        logging.error(f"HTTP request failed: {e}")
        return {}


    # Comprobar y devolver la respuesta
    if response.ok:
        return response.json()
    else:
        print(f"Request failed with status code {response.status_code}: {response.text}")
        response.raise_for_status()

def group_block(group, action, group_id_override=None, node = None):
    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":
        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:
        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):
    src_name     = connectivity_service.get("source", "FALTA VALOR")
    dest_list    = connectivity_service.get("destination", ["FALTA VALOR"])
    dest_str     = ",".join(dest_list)
    config_rules = []

    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": []
    }

    attributes = connectivity_service["connectivity-service"]["tapi-connectivity:connectivity-service"]["connection"][0]["optical-connection-attributes"]
    groups = attributes["subcarrier-attributes"]["digital-subcarrier-group"]
    # central_freq = (attributes["central-frequency"])
    # tx_power = (attributes["Tx-power"])
    # spacing = attributes["digital-subcarrier-spacing"]
    operational_mode = attributes["modulation"]["operational-mode"]
    # port = attributes["modulation"]["port"]
    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
    }

    leaves = []
    for dest, group in zip(connectivity_service["destination"], groups):
        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':
        provisionamiento["actions"].append({
            "type":   "XR_AGENT_ACTIVATE_TRANSCEIVER",
            "layer":  "OPTICAL",
            "content": final_json,
            "controller-uuid": "IPoWDM Controller"
        })

        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
                nodes[node] = {
                    "ip-address": ip,
                    "ip-mask":    prefix,
                    "vlan-id":    vlan
                }
            
        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:
        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

def get_sip_from_name(context, node_name):

    context = context.json()
    topologies = context.get("tapi-topology:topology-context", {}).get("topology", [])
    for topology in topologies:
        nodes = topology.get("nodes", [])
        for node in nodes:
            if node.get("name") == node_name:
                return node.get("uuid")
    return None  
Loading