Commit a7b6056a authored by Pablo Armingol's avatar Pablo Armingol
Browse files

feat(e2e): Implement E2E orchestrator functionality

- Added e2e_connect.py to handle E2E requests to the controller.
- Created main.py to manage E2E realization logic based on intent and rules.
- Introduced service_types for L3oWDM and L2VPN slices, including deletion capabilities.
- Developed templates for Optical and IPoWDM services to standardize requests.
- Enhanced main.py and select_way.py to support E2E controller type.
- Updated send_controller.py to route requests to the E2E controller.
- Added E2E namespace in Swagger for API documentation and interaction.
- Modified build_response.py to accommodate E2E-specific response structures.
parent 6883420e
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ from flask_restx import Api
from flask_cors import CORS
from swagger.tfs_namespace import tfs_ns
from swagger.ixia_namespace import ixia_ns
from swagger.E2E_namespace import e2e_ns
from src.config.constants import NSC_PORT
from src.webui.gui import gui_bp
from src.config.config import create_config
@@ -50,6 +51,7 @@ def create_app():
    # Register namespaces
    api.add_namespace(tfs_ns, path="/tfs")
    api.add_namespace(ixia_ns, path="/ixia")
    api.add_namespace(e2e_ns, path="/e2e")

    if app.config["WEBUI_DEPLOY"]:
        app.secret_key = "clave-secreta-dev"
+2 −0
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ def create_config(app: Flask):
    # Mapper
    app.config["NRP_ENABLED"] = os.getenv("NRP_ENABLED", "false").lower() == "true"
    app.config["PLANNER_ENABLED"] = os.getenv("PLANNER_ENABLED", "false").lower() == "true"
    app.config["PLANNER_TYPE"] = os.getenv("PLANNER_TYPE", "ENERGY")
    app.config["PCE_EXTERNAL"] = os.getenv("PCE_EXTERNAL", "false").lower() == "true"

    # Realizer
@@ -51,6 +52,7 @@ def create_config(app: Flask):
    app.config["TFS_IP"] = os.getenv("TFS_IP", "127.0.0.1")
    app.config["UPLOAD_TYPE"] = os.getenv("UPLOAD_TYPE", "WEBUI")
    app.config["TFS_L2VPN_SUPPORT"] = os.getenv("TFS_L2VPN_SUPPORT", "false").lower() == "true"
    app.config["TFS_E2E"] = os.getenv("TFS_E2E", "127.0.0.1")

    # IXIA
    app.config["IXIA_IP"] = os.getenv("IXIA_IP", "127.0.0.1")
+18 −17
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@

# This file includes original contributions from Telefonica Innovación Digital S.L.

import logging
import time
from src.utils.dump_templates import dump_templates
from src.utils.build_response import build_response
@@ -94,12 +95,12 @@ class NSController:

        for intent in ietf_intents:
            # Mapper
            mapper(intent)
            rules = mapper(intent)
            # Store slice request details and build response
            store_data(intent, slice_id, controller_type=self.controller_type)
            self.response = build_response(intent, self.response)
            self.response = build_response(intent, self.response, controller_type= self.controller_type)
            # Realizer
            request = realizer(intent, controller_type=self.controller_type, response = self.response)
            request = realizer(intent, controller_type=self.controller_type, response = self.response, rules = rules)
            requests["services"].append(request)

        # Store the generated template for debugging
+10 −8
Original line number Diff line number Diff line
@@ -69,5 +69,7 @@ def mapper(ietf_intent):
                # 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)
        optimal_path = Planner().planner(ietf_intent, current_app.config["PLANNER_TYPE"])
        logging.debug(f"Optimal path: {optimal_path}")
        return optimal_path
    return None
 No newline at end of file
+275 −0
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, random, os, json, heapq  
from src.config.constants import SRC_PATH
from flask import current_app


def energy_planner(self, intent):    
    energy_metrics = self.__retrieve_energy()
    topology = self.__retrieve_topology()
    source = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[0].get("id") or "A"
    destination = intent.get("ietf-network-slice-service:network-slice-services", {}).get("slice-service", [])[0].get("sdps", {}).get("sdp", [])[1].get("id") or "B"
    optimal_path = []
    # If using an external PCE
    if current_app.config["PCE_EXTERNAL"]:
        logging.debug("Using external PCE for path planning")    
        def build_slice_input(node_source, node_destination):
            return {
                "clientName": "demo-client",
                "requestId": random.randint(1000, 9999),
                "sites": [node_source["nodeId"], node_destination["nodeId"]],
                "graph": {
                    "nodes": [
                        {
                            "nodeId": node_source["nodeId"],
                            "name": node_source["name"],
                            "footprint": node_source["footprint"],
                            "sticky": [node_source["nodeId"]]
                        },
                        {
                            "nodeId": node_destination["nodeId"],
                            "name": node_destination["name"],
                            "footprint": node_destination["footprint"],
                            "sticky": [node_destination["nodeId"]]
                        }
                    ],
                    "links": [
                        {
                            "fromNodeId": node_source["nodeId"],
                            "toNodeId": node_destination["nodeId"],
                            "bandwidth": 1000000000,
                            "metrics": [
                                {
                                    "metric": "DELAY",
                                    "value": 10,
                                    "bound": True,
                                    "required": True
                                }
                            ]
                        }
                    ],
                    "constraints": {
                        "maxVulnerability": 3,
                        "maxDeployedServices": 10,
                        "metricLimits": []
                    }
                }
            }
        source = next((node for node in topology["nodes"] if node["name"] == source), None)
        destination = next((node for node in topology["nodes"] if node["name"] == destination), None)
        slice_input = build_slice_input(source, destination)

        # POST /sss/v1/slice/compute
        def simulate_slice_output(input_data):
            return {
                "input": input_data,
                "slice": {
                    "nodes": [
                        {"site": 1, "service": 1},
                        {"site": 2, "service": 2}
                    ],
                    "links": [
                        {
                            "fromNodeId": 1,
                            "toNodeId": 2,
                            "lspId": 500,
                            "path": {
                                "ingressNodeId": 1,
                                "egressNodeId": 2,
                                "hops": [
                                    {"nodeId": 3, "linkId": "A-C", "portId": 1},
                                    {"nodeId": 2, "linkId": "C-B", "portId": 2}
                                ]
                            }
                        }
                    ],
                    "metric": {"value": 9}
                },
                "error": None
            }
        slice_output = simulate_slice_output(slice_input)
        # Mostrar resultado
        optimal_path.append(source["name"])
        for link in slice_output["slice"]["links"]:
            for hop in link["path"]["hops"]:
                optimal_path.append(next((node for node in topology["nodes"] if node["nodeId"] == hop['nodeId']), None)["name"])
    
    else:
        logging.debug("Using internal PCE for path planning")
        ietf_dlos = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"]
        logging.debug(ietf_dlos),
        # Solo asigna los DLOS que existan, el resto a None
        dlos = {
            "EC": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_consumption"), None),
            "CE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "carbon_emission"), None),
            "EE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "energy_efficiency"), None),
            "URE": next((item.get("bound") for item in ietf_dlos if item.get("metric-type") == "renewable_energy_usage"), None)
        }
        logging.debug(f"Planning optimal path from {source} to {destination} with DLOS: {dlos}")
        optimal_path = self.__calculate_optimal_path(topology, energy_metrics, source, destination, dlos)

    if not optimal_path:
        logging.error("No valid path found")
        raise Exception("No valid energy path found")

    return optimal_path

def __retrieve_energy(self):
    # TODO : Implement the logic to retrieve energy consumption data from controller
    # Taking it from static file
    with open(os.path.join(SRC_PATH, "planner/energy_ddbb.json"), "r") as archivo:
        energy_metrics = json.load(archivo)
    return energy_metrics

def __retrieve_topology(self):
    if current_app.config["PCE_EXTERNAL"]:
        # TODO : Implement the logic to retrieve topology data from external PCE
        # GET /sss/v1/topology/node and /sss/v1/topology/link
        with open(os.path.join(SRC_PATH, "planner/ext_topo_ddbb.json"), "r") as archivo:
            topology = json.load(archivo)
    else:
        # TODO : Implement the logic to retrieve topology data from controller
        # Taking it from static file
        with open(os.path.join(SRC_PATH, "planner/topo_ddbb.json"), "r") as archivo:
            topology = json.load(archivo)
    return topology



def __calculate_optimal_path(self, topology, energy_metrics, source, destination, dlos):
    logging.debug("Starting optimal path calculation...")
    
    # Create a dictionary with the weights of each node
    node_data_map = {}
    for node_data in energy_metrics:
        node_id = node_data["name"]
        ec = node_data["typical-power"]
        ce = node_data["carbon-emissions"]
        ee = node_data["efficiency"]
        ure = node_data["renewable-energy-usage"]

        total_power_supply = sum(ps["typical-power"] for ps in node_data["power-supply"])
        total_power_boards = sum(b["typical-power"] for b in node_data["boards"])
        total_power_components = sum(c["typical-power"] for c in node_data["components"])
        total_power_transceivers = sum(t["typical-power"] for t in node_data["transceivers"])

        logging.debug(f"Node {node_id}: EC={ec}, CE={ce}, EE={ee}, URE={ure}")
        logging.debug(f"Node {node_id}: PS={total_power_supply}, BO={total_power_boards}, CO={total_power_components}, TR={total_power_transceivers}")

        weight = self.__compute_node_weight(ec, ce, ee, ure,
                                            total_power_supply,
                                            total_power_boards,
                                            total_power_components,
                                            total_power_transceivers)
        logging.debug(f"Weight for node {node_id}: {weight}")
        
        node_data_map[node_id] = {
            "weight": weight,
            "ec": ec,
            "ce": ce,
            "ee": ee,
            "ure": ure
        }

    # Create a graph representation of the topology
    graph = {}
    for node in topology["ietf-network:networks"]["network"][0]["node"]:
        graph[node["node-id"]] = []
    for link in topology["ietf-network:networks"]["network"][0]["link"]:
        src = link["source"]["source-node"]
        dst = link["destination"]["dest-node"]
        graph[src].append((dst, node_data_map[dst]["weight"]))
        logging.debug(f"Added link: {src} -> {dst} with weight {node_data_map[dst]['weight']}")

    # Dijkstra's algorithm with restrictions
    queue = [(0, source, [], 0, 0, 0, 1)]  # (accumulated cost, current node, path, sum_ec, sum_ce, sum_ee, min_ure)
    visited = set()

    logging.debug(f"Starting search from {source} to {destination} with restrictions: {dlos}")
    

    while queue:
        cost, node, path, sum_ec, sum_ce, sum_ee, min_ure = heapq.heappop(queue)
        logging.debug(f"Exploring node {node} with cost {cost} and path {path + [node]}")
        
        if node in visited:
            logging.debug(f"Node {node} already visited, skipped.")
            continue
        visited.add(node)
        path = path + [node]

        node_metrics = node_data_map[node]
        sum_ec += node_metrics["ec"]
        sum_ce += node_metrics["ce"]
        sum_ee += node_metrics["ee"]
        min_ure = min(min_ure, node_metrics["ure"]) if path[:-1] else node_metrics["ure"]

        logging.debug(f"Accumulated -> EC: {sum_ec}, CE: {sum_ce}, EE: {sum_ee}, URE min: {min_ure}")

        if dlos["EC"] is not None and sum_ec > dlos["EC"]:
            logging.debug(f"Discarded path {path} for exceeding EC ({sum_ec} > {dlos['EC']})")
            continue
        if dlos["CE"] is not None and sum_ce > dlos["CE"]:
            logging.debug(f"Discarded path {path} for exceeding CE ({sum_ce} > {dlos['CE']})")
            continue
        if dlos["EE"] is not None and sum_ee > dlos["EE"]:
            logging.debug(f"Discarded path {path} for exceeding EE ({sum_ee} > {dlos['EE']})")
            continue
        if dlos["URE"] is not None and min_ure < dlos["URE"]:
            logging.debug(f"Discarded path {path} for not reaching minimum URE ({min_ure} < {dlos['URE']})")
            continue

        if node == destination:
            logging.debug(f"Destination {destination} reached with a valid path: {path}")
            return path

        for neighbor, weight in graph.get(node, []):
            if neighbor not in visited:
                logging.debug(f"Qeue -> neighbour: {neighbor}, weight: {weight}")
                heapq.heappush(queue, (
                    cost + weight,
                    neighbor,
                    path,
                    sum_ec,
                    sum_ce,
                    sum_ee,
                    min_ure
                ))
    logging.debug("No valid path found that meets the restrictions.")
    return []


def __compute_node_weight(self, ec, ce, ee, ure, total_power_supply, total_power_boards, total_power_components, total_power_transceivers, alpha=1, beta=1, gamma=1, delta=1):
    """
    Calcula el peso de un nodo con la fórmula:
    w(v) = α·EC + β·CE + γ/EE + δ·(1 - URE)
    """
    traffic = 100
    # Measure one hour of traffic
    time = 1

    power_idle = ec + total_power_supply + total_power_boards + total_power_components + total_power_transceivers
    power_traffic = traffic * ee

    power_total = (power_idle + power_traffic)

    green_index = power_total * time / 1000 * (1 - ure) * ce

    return green_index 

Loading