Loading proto/e2eorchestrator.proto +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import "context.proto"; service E2EOrchestratorService { rpc Compute(E2EOrchestratorRequest) returns (E2EOrchestratorReply) {} rpc PushTopology(context.Topology) returns (context.Empty) {} rpc RunPathAlgorithm(E2EOrchestratorRequest) returns (E2EOrchestratorReply) {} } message E2EOrchestratorRequest { Loading src/e2e_orchestrator/client/E2EOrchestratorClient.py +19 −0 Original line number Diff line number Diff line Loading @@ -67,3 +67,22 @@ class E2EOrchestratorClient: "Compute result: {:s}".format(str(grpc_message_to_json(response))) ) return response def RunPathAlgorithm(self, service: Service) -> E2EOrchestratorReply: #request = E2EOrchestratorRequest(service=service) #LOGGER.info("PathAlgorithm request: %s", grpc_message_to_json(request)) #response = self.stub.RunPathAlgorithm(request) # si el proto define RunPathAlgorithm #LOGGER.info("PathAlgorithm result: %s", grpc_message_to_json(response)) #return response LOGGER.info("Llegó a RunPathAlgorithm con service: %s", service.service_id.service_uuid.uuid) # Retorno simulado para no bloquear class FakeReply: def __init__(self): self.message = "Prueba completada" return FakeReply() return response src/nbi/service/etsi_e2e_path_computation/Resources.py 0 → 100644 +113 −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. 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 ) 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 } service = e2epathcomp_2_service(self.client, service_payload) try: # Llamar a PathAlgorithm en vez de crear o actualizar el servicio directamente from common.client.E2eOrchestratorClient import E2EOrchestratorClient 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)) src/nbi/service/etsi_e2e_path_computation/Tools.py 0 → 100644 +109 −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. 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))) src/nbi/service/etsi_e2e_path_computation/__init__.py 0 → 100644 +30 −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. from nbi.service.NbiApplication import NbiApplication from .Resources import E2epathcomp, E2epathcompId URL_PREFIX = '/restconf/e2epathcomp/v0' def register_etsi_e2e_path_computation(nbi_app : NbiApplication): nbi_app.add_rest_api_resource( E2epathcompInfo, URL_PREFIX + '/bw_allocations', endpoint='etsi_bwm.bw_info' ) nbi_app.add_rest_api_resource( E2epathcompInfoId, URL_PREFIX + '/bw_allocations/<path:allocationId>', endpoint='etsi_bwm.bw_info_id' ) Loading
proto/e2eorchestrator.proto +1 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import "context.proto"; service E2EOrchestratorService { rpc Compute(E2EOrchestratorRequest) returns (E2EOrchestratorReply) {} rpc PushTopology(context.Topology) returns (context.Empty) {} rpc RunPathAlgorithm(E2EOrchestratorRequest) returns (E2EOrchestratorReply) {} } message E2EOrchestratorRequest { Loading
src/e2e_orchestrator/client/E2EOrchestratorClient.py +19 −0 Original line number Diff line number Diff line Loading @@ -67,3 +67,22 @@ class E2EOrchestratorClient: "Compute result: {:s}".format(str(grpc_message_to_json(response))) ) return response def RunPathAlgorithm(self, service: Service) -> E2EOrchestratorReply: #request = E2EOrchestratorRequest(service=service) #LOGGER.info("PathAlgorithm request: %s", grpc_message_to_json(request)) #response = self.stub.RunPathAlgorithm(request) # si el proto define RunPathAlgorithm #LOGGER.info("PathAlgorithm result: %s", grpc_message_to_json(response)) #return response LOGGER.info("Llegó a RunPathAlgorithm con service: %s", service.service_id.service_uuid.uuid) # Retorno simulado para no bloquear class FakeReply: def __init__(self): self.message = "Prueba completada" return FakeReply() return response
src/nbi/service/etsi_e2e_path_computation/Resources.py 0 → 100644 +113 −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. 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 ) 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 } service = e2epathcomp_2_service(self.client, service_payload) try: # Llamar a PathAlgorithm en vez de crear o actualizar el servicio directamente from common.client.E2eOrchestratorClient import E2EOrchestratorClient 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))
src/nbi/service/etsi_e2e_path_computation/Tools.py 0 → 100644 +109 −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. 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)))
src/nbi/service/etsi_e2e_path_computation/__init__.py 0 → 100644 +30 −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. from nbi.service.NbiApplication import NbiApplication from .Resources import E2epathcomp, E2epathcompId URL_PREFIX = '/restconf/e2epathcomp/v0' def register_etsi_e2e_path_computation(nbi_app : NbiApplication): nbi_app.add_rest_api_resource( E2epathcompInfo, URL_PREFIX + '/bw_allocations', endpoint='etsi_bwm.bw_info' ) nbi_app.add_rest_api_resource( E2epathcompInfoId, URL_PREFIX + '/bw_allocations/<path:allocationId>', endpoint='etsi_bwm.bw_info_id' )