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

refactor: replace ETSI E2E path computation with optical E2E path computation...

refactor: replace ETSI E2E path computation with optical E2E path computation and update related services
parent fb52a1c8
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -49,8 +49,8 @@ from .tfs_api import register_tfs_api
#from .topology_updates import register_topology_updates
from .vntm_recommend import register_vntm_recommend
from .well_known_meta import register_well_known
from .etsi_e2e_path_computation import register_etsi_e2e_path_computation
from .media_channel import register_media_channel
from .optical_e2e_path_computation import register_e2e_path_computation

LOG_LEVEL = get_log_level()
logging.basicConfig(
@@ -108,9 +108,9 @@ register_telemetry_subscription(nbi_app)
register_tfs_api         (nbi_app)
#register_topology_updates(nbi_app) # does not work; check if eventlet-grpc side effects
register_vntm_recommend  (nbi_app)
register_etsi_e2e_path_computation (nbi_app)
register_media_channel   (nbi_app)
register_well_known      (nbi_app)
register_e2e_path_computation(nbi_app)
LOGGER.info('All connectors registered')

nbi_app.dump_configuration()
+0 −115
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.

import copy, deepmerge, json, logging
from typing import Dict
from flask_restful import Resource, request
from werkzeug.exceptions import UnsupportedMediaType
from common.Constants import DEFAULT_CONTEXT_NAME
from context.client.ContextClient import ContextClient
from service.client.ServiceClient import ServiceClient
from .Tools import (
    format_grpc_to_json, grpc_context_id, grpc_service_id, e2epathcomp_2_service, service_2_e2epathcomp
)
from e2e_orchestrator.client.E2EOrchestratorClient import E2EOrchestratorClient

LOGGER = logging.getLogger(__name__)


class _Resource(Resource):
    def __init__(self) -> None:
        super().__init__()
        self.client = ContextClient()
        self.service_client = ServiceClient()


class E2epathcomp(_Resource):
    def get(self):
        service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME))
        bw_allocations = [service_2_e2epathcomp(service) for service in service_list.services]
        return bw_allocations

    def post(self):
        if not request.is_json:
            raise UnsupportedMediaType('JSON payload is required')
        
        data: Dict = request.get_json()
        origen = data.get('origen')
        destino = data.get('destino')
        bw = data.get('bw')
        app_ins_id = data.get('appInsId', 'tmp_uuid')

        if not all([origen, destino, bw]):
            return {"error": "Los campos 'origen', 'destino' y 'bw' son obligatorios"}, 400

        # Construir el Service usando los datos recibidos
        service_payload = {
            'origen': origen,
            'destino': destino,
            'fixedAllocation': bw,
            'appInsId': app_ins_id
        }

        LOGGER.info("Llegó al Resources POST RunPathAlgorithm con payload: %s", service_payload)

        service = e2epathcomp_2_service(service_payload)

        try:
            # Llamar a PathAlgorithm en vez de crear o actualizar el servicio directamente
            
            e2e_client = E2EOrchestratorClient()
            response = e2e_client.RunPathAlgorithm(service)  # Llamada al algoritmo de PathAlgorithm
            response_json = format_grpc_to_json(response)
            e2e_client.close()
        except Exception as e:  # pylint: disable=broad-except
            LOGGER.exception("Error ejecutando PathAlgorithm")
            return {"error": str(e)}, 500

        return response_json


class E2epathcompId(_Resource):

    def get(self, allocationId: str):
        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId))
        return service_2_e2epathcomp(service)

    def put(self, allocationId: str):
        json_data = json.loads(request.get_json())
        service = e2epathcomp_2_service(self.client, json_data)
        self.service_client.UpdateService(service)
        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, json_data['appInsId']))
        response_bwm = service_2_e2epathcomp(service)

        return response_bwm

    def patch(self, allocationId: str):
        json_data = request.get_json()
        if not 'appInsId' in json_data:
            json_data['appInsId'] = allocationId
        
        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, json_data['appInsId']))
        current_bwm = service_2_e2epathcomp(service)
        new_bmw = deepmerge.always_merger.merge(current_bwm, json_data)
        
        service = e2epathcomp_2_service(self.client, new_bmw)
        self.service_client.UpdateService(service)

        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, json_data['appInsId']))
        response_bwm = service_2_e2epathcomp(service)

        return response_bwm

    def delete(self, allocationId: str):
        self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId))
+0 −109
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.

import json, logging, time
from decimal import ROUND_HALF_EVEN, Decimal
from flask.json import jsonify
from common.proto.context_pb2 import (
    ContextId, ServiceId, ServiceStatusEnum, ServiceTypeEnum,
    Service, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom,
    ConfigActionEnum
)
from common.tools.grpc.Tools import grpc_message_to_json
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.Service import json_service_id

LOGGER = logging.getLogger(__name__)

# ---------- Funciones adaptadas al flujo E2E/PathComputation ----------

def service_2_e2epathcomp(service: Service) -> dict:
    """Extrae la información mínima de un Service para E2E"""
    info = {
        "appInsId": service.service_id.service_uuid.uuid,
        "fixedAllocation": None,
        "origen": None,
        "destino": None
    }

    # Extraer ancho de banda
    for c in service.service_constraints:
        if c.WhichOneof('constraint') == 'sla_capacity':
            fixed_allocation = Decimal(c.sla_capacity.capacity_gbps * 1.e9)
            info["fixedAllocation"] = str(fixed_allocation.quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
            break

    # Extraer origen/destino
    for cr in service.service_config.config_rules:
        if cr.WhichOneof('config_rule') != 'custom':
            continue
        val = json.loads(cr.custom.resource_value)
        if "sessionFilter" in val:
            info["origen"] = val["sessionFilter"].get("sourceIp")
            info["destino"] = val["sessionFilter"].get("dstAddress")
            break

    # Añadir timestamp
    unixtime = time.time()
    info["timeStamp"] = {
        "seconds": int(unixtime),
        "nanoseconds": int(unixtime % 1 * 1e9)
    }

    return info


def e2epathcomp_2_service(payload: dict) -> Service:
    """Crea un Service mínimo a partir de un payload E2E (origen, destino, bw, appInsId)"""
    service = Service()
    service.service_id.service_uuid.uuid = payload.get("appInsId", "tmp_uuid")
    service.service_id.context_id.context_uuid.uuid = "admin"
    service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM
    service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED

    # Añadir ancho de banda
    if "fixedAllocation" in payload:
        capacity = Constraint_SLA_Capacity()
        capacity.capacity_gbps = float(payload["fixedAllocation"]) / 1e9
        constraint = Constraint()
        constraint.sla_capacity.CopyFrom(capacity)
        service.service_constraints.append(constraint)

    # Añadir sessionFilter
    if "origen" in payload and "destino" in payload:
        cr = ConfigRule()
        cr.action = ConfigActionEnum.CONFIGACTION_SET
        cr_custom = ConfigRule_Custom()
        cr_custom.resource_key = "/request"
        cr_custom.resource_value = json.dumps({
            "sessionFilter": {"sourceIp": payload["origen"], "dstAddress": payload["destino"]}
        })
        cr.custom.CopyFrom(cr_custom)
        service.service_config.config_rules.append(cr)

    return service


# ---------- Funciones auxiliares ----------

def format_grpc_to_json(grpc_reply):
    return jsonify(grpc_message_to_json(grpc_reply))


def grpc_context_id(context_uuid):
    return ContextId(**json_context_id(context_uuid))


def grpc_service_id(context_uuid, service_uuid):
    return ServiceId(**json_service_id(service_uuid, context_id=json_context_id(context_uuid)))
+0 −106
Original line number Diff line number Diff line
-----------------------GET-----------------------

curl --request GET \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations

_______________________POST E2EOrchestrator________________________



curl --request POST \
  --url http://192.168.165.44/restconf/e2epathcomp/v0/e2e_path_computation \
  --header 'Content-Type: application/json' \
  --data '{
    "origen": "192.168.1.2",
    "destino": "192.168.3.2",
    "bw": "1000",
    "appInsId": "service_uuid_123"
  }'













-----------------------POST-----------------------
curl --request POST \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations \
  --header 'Content-Type: application/json' \
  --data '{
  "allocationDirection": "string",
  "appInsId": "service_uuid",
  "fixedAllocation": "123",
  "fixedBWPriority": "SEE_DESCRIPTION",
  "requestType": 0,
  "sessionFilter": [
    {
      "dstAddress": "192.168.3.2",
      "dstPort": [
        "b"
      ],
      "protocol": "string",
      "sourceIp": "192.168.1.2",
      "sourcePort": [
        "a"
      ]
    }
  ],
  "timeStamp": {
    "nanoSeconds": 1,
    "seconds": 1
  }
}'


-----------------------GET2-----------------------
curl --request GET \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid

-----------------------PUT-----------------------
  curl --request PUT \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid \
  --header 'Content-Type: application/json' \
  --data '{
  "allocationDirection": "string",
  "appInsId": "service_uuid",
  "fixedAllocation": "123",
  "fixedBWPriority": "efefe",
  "requestType": 0,
  "sessionFilter": [
    {
      "dstAddress": "192.168.3.2",
      "dstPort": [
        "b"
      ],
      "protocol": "string",
      "sourceIp": "192.168.1.2",
      "sourcePort": [
        "a"
      ]
    }
  ],
  "timeStamp": {
    "nanoSeconds": 1,
    "seconds": 1
  }
}'

-----------------------PATCH-----------------------
curl --request PATCH \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid \
  --header 'Content-Type: application/json' \
  --data '{
  "fixedBWPriority": "uuuuuuuuuuuuuu"
}'


-----------------------DELETE-----------------------
curl --request DELETE \
  --url http://10.1.7.203:80/restconf/bwm/v1/bw_allocations/service_uuid
 No newline at end of file
+69 −0
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.

import logging
import json
from flask import request
from flask_restful import Resource
from common.proto.context_pb2 import Service, ServiceTypeEnum, ConfigActionEnum, ConfigRule
from common.proto.e2eorchestrator_pb2 import E2EOrchestratorRequest
from e2e_orchestrator.client.E2EOrchestratorClient import E2EOrchestratorClient
from common.tools.grpc.Tools import grpc_message_to_json

LOGGER = logging.getLogger(__name__)

class E2epathcomp(Resource):
    def __init__(self):
        super().__init__()
        self.e2e_client = E2EOrchestratorClient()

    def post(self):
        data = request.get_json()
        LOGGER.info(f"Received E2E Optical Path Computation request: {json.dumps(data, indent=2)}")

        try:
            # Construct a Service protobuf to encapsulate the intent for the E2E Orchestrator
            service = Service()
            service.service_id.service_uuid.uuid = "e2e-optical-service"
            service.service_id.context_id.context_uuid.uuid = "admin"
            service.service_type = ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY
            
            # Pack the original JSON payload into a config rule so E2E Orchestrator and PathComp can access it
            config_rule = ConfigRule()
            config_rule.action = ConfigActionEnum.CONFIGACTION_SET
            config_rule.custom.resource_key = "intent"
            config_rule.custom.resource_value = json.dumps(data)
            service.service_config.config_rules.append(config_rule)

            req = E2EOrchestratorRequest(service=service)
            
            LOGGER.info("Sending request to E2E Orchestrator Compute...")
            reply = self.e2e_client.Compute(req)
            
            reply_json = grpc_message_to_json(reply)
            
            # Check if there is an optical_path_result injected by PathComp
            if reply.services:
                for cr in reply.services[0].service_config.config_rules:
                    if cr.WhichOneof('config_rule') == 'custom' and cr.custom.resource_key == "optical_path_result":
                        LOGGER.info("Found optical_path_result, returning it directly.")
                        return json.loads(cr.custom.resource_value), 200

            LOGGER.info(f"Received reply from E2E Orchestrator: {json.dumps(reply_json)}")
            # The NBI returns the standard protobuf JSON response if no custom result is found
            return reply_json, 200

        except Exception as e:
            LOGGER.error(f"Error calling E2E Orchestrator: {str(e)}", exc_info=True)
            return {"status": "error", "message": str(e)}, 500
Loading