diff --git a/deploy/all.sh b/deploy/all.sh index a284287bc1a870d1c999e26dadc9d957b4eb974f..8bd103fee25252171bb6a964902dcab136a3aa69 100755 --- a/deploy/all.sh +++ b/deploy/all.sh @@ -27,7 +27,7 @@ export TFS_REGISTRY_IMAGES=${TFS_REGISTRY_IMAGES:-"http://localhost:32000/tfs/"} # If not already set, set the list of components, separated by spaces, you want to build images for, and deploy. # By default, only basic components are deployed -export TFS_COMPONENTS=${TFS_COMPONENTS:-"context device pathcomp service slice nbi webui load_generator"} +export TFS_COMPONENTS=${TFS_COMPONENTS:-"context device pathcomp service slice nbi webui load_generator e2e_orchestrator"} # Uncomment to activate Monitoring (old) #export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring" diff --git a/my_deploy.sh b/my_deploy.sh index 662dc389b123daabe02bedf2f43232edde8f3bc3..5aca553ce459a830983093f802de8bf1ff369d79 100644 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -20,7 +20,7 @@ export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/" # Set the list of components, separated by spaces, you want to build images for, and deploy. -export TFS_COMPONENTS="context device pathcomp service slice nbi webui" +export TFS_COMPONENTS="context device pathcomp service slice nbi webui e2e_orchestrator" # Uncomment to activate Monitoring (old) #export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring" diff --git a/proto/e2eorchestrator.proto b/proto/e2eorchestrator.proto index 93e30998a645d17c78c72c6187a82cc678fee61f..3cf1709870af39900e0b079ae686df2d28f4de40 100644 --- a/proto/e2eorchestrator.proto +++ b/proto/e2eorchestrator.proto @@ -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 { diff --git a/src/e2e_orchestrator/client/E2EOrchestratorClient.py b/src/e2e_orchestrator/client/E2EOrchestratorClient.py index 6e959a6f5d23ba773eb421c03ad3477ba4ede7db..306584083af4e0ba653c5d21840ce4dbeec4ba05 100644 --- a/src/e2e_orchestrator/client/E2EOrchestratorClient.py +++ b/src/e2e_orchestrator/client/E2EOrchestratorClient.py @@ -23,6 +23,10 @@ from common.Settings import get_service_host, get_service_port_grpc from common.tools.client.RetryDecorator import delay_exponential, retry from common.tools.grpc.Tools import grpc_message_to_json from common.proto.e2eorchestrator_pb2 import E2EOrchestratorRequest, E2EOrchestratorReply +from common.proto.context_pb2 import Service + +from decimal import Decimal, ROUND_HALF_EVEN +import json LOGGER = logging.getLogger(__name__) MAX_RETRIES = 15 @@ -67,3 +71,48 @@ class E2EOrchestratorClient: "Compute result: {:s}".format(str(grpc_message_to_json(response))) ) return response + + def RunPathAlgorithm(self, service: Service) -> dict: + # Extraer appInsId + appInsId = service.service_id.service_uuid.uuid + + # Extraer fixedAllocation + bw = None + for c in service.service_constraints: + if c.WhichOneof('constraint') == 'sla_capacity': + bw = str(Decimal(c.sla_capacity.capacity_gbps * 1.e9).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN)) + break + + # Extraer origen/destino + origen = None + destino = None + 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: + origen = val["sessionFilter"].get("sourceIp") + destino = val["sessionFilter"].get("dstAddress") + break + + LOGGER.info( + "Llegó a RunPathAlgorithm con service_id=%s, origen=%s, destino=%s, bw=%s", + appInsId, origen, destino, bw + ) + + # Retorno simulado para pruebas + reply = { + "origen": origen, + "destino": destino, + "bw": bw, + "appInsId": appInsId, + "message": "Recibido correctamente" + } + return reply + + #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 + diff --git a/src/nbi/Dockerfile b/src/nbi/Dockerfile index 63556432be46fae44552bb2ec191e3f7eab17a99..444242dbae75814667802e6de1c91398d65a05c7 100644 --- a/src/nbi/Dockerfile +++ b/src/nbi/Dockerfile @@ -87,6 +87,10 @@ COPY src/qos_profile/__init__.py qos_profile/__init__.py COPY src/qos_profile/client/. qos_profile/client/ COPY src/vnt_manager/__init__.py vnt_manager/__init__.py COPY src/vnt_manager/client/. vnt_manager/client/ + +COPY src/e2e_orchestrator/. e2e_orchestrator/__init__.py +COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/ + RUN mkdir -p /var/teraflow/tests/tools COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/ diff --git a/src/nbi/service/app.py b/src/nbi/service/app.py index 2d6102a3492a8e29bf88682d1ae0cec0e327b8de..73b2a60c70e69d0bc9241515a97faf5602474d5e 100644 --- a/src/nbi/service/app.py +++ b/src/nbi/service/app.py @@ -44,6 +44,7 @@ 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 LOG_LEVEL = get_log_level() @@ -98,6 +99,7 @@ register_qkd_app (nbi_app) #register_topology_updates(nbi_app) # does not work; check if eventlet-grpc side effects register_vntm_recommend (nbi_app) register_camara_qod (nbi_app) +register_etsi_e2e_path_computation (nbi_app) LOGGER.info('All connectors registered') nbi_app.dump_configuration() diff --git a/src/nbi/service/etsi_e2e_path_computation/Resources.py b/src/nbi/service/etsi_e2e_path_computation/Resources.py new file mode 100644 index 0000000000000000000000000000000000000000..d0d86aba706a1db153ceb3cc9645afcd474b0304 --- /dev/null +++ b/src/nbi/service/etsi_e2e_path_computation/Resources.py @@ -0,0 +1,115 @@ +# 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)) diff --git a/src/nbi/service/etsi_e2e_path_computation/Tools.py b/src/nbi/service/etsi_e2e_path_computation/Tools.py new file mode 100644 index 0000000000000000000000000000000000000000..a740ae6e0b53b4001bffb776415a43f6cc26e897 --- /dev/null +++ b/src/nbi/service/etsi_e2e_path_computation/Tools.py @@ -0,0 +1,109 @@ +# 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))) diff --git a/src/nbi/service/etsi_e2e_path_computation/__init__.py b/src/nbi/service/etsi_e2e_path_computation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b66c6187ac520cfcf53934127b82e3b0e290016a --- /dev/null +++ b/src/nbi/service/etsi_e2e_path_computation/__init__.py @@ -0,0 +1,26 @@ +# 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 # solo necesitamos este + +URL_PREFIX = '/restconf/e2epathcomp/v0' + +def register_etsi_e2e_path_computation(nbi_app: NbiApplication): + nbi_app.add_rest_api_resource( + E2epathcomp, + URL_PREFIX + '/e2e_path_computation', + endpoint='etsi_e2e_path_computation' + ) + diff --git a/src/nbi/service/etsi_e2e_path_computation/tests_etsi_bwm.txt b/src/nbi/service/etsi_e2e_path_computation/tests_etsi_bwm.txt new file mode 100644 index 0000000000000000000000000000000000000000..bedc08ee1d5c799ac44253417d56e08deda976a1 --- /dev/null +++ b/src/nbi/service/etsi_e2e_path_computation/tests_etsi_bwm.txt @@ -0,0 +1,106 @@ +-----------------------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