From 7e08fbf94bdc3eb44bd6c488824507a2b92f41b9 Mon Sep 17 00:00:00 2001 From: rahhal Date: Tue, 15 Oct 2024 15:22:56 +0000 Subject: [PATCH 01/19] First verison of CAMARA QoD NBI connector --- manifests/nbiservice.yaml | 2 +- manifests/qos_profileservice.yaml | 2 +- my_deploy.sh | 6 +- src/nbi/Dockerfile | 2 + src/nbi/service/__main__.py | 2 + .../nbi_plugins/camara_qod/Resources.py | 257 ++++++++++++++++++ .../nbi_plugins/camara_qod/Tools.py | 195 +++++++++++++ .../nbi_plugins/camara_qod/__init__.py | 36 +++ src/nbi/tests/test_camara_qod_profile.py | 226 +++++++++++++++ src/nbi/tests/test_camara_qos_service.py | 52 ++++ 10 files changed, 775 insertions(+), 5 deletions(-) mode change 100755 => 100644 my_deploy.sh create mode 100644 src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py create mode 100644 src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py create mode 100644 src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py create mode 100644 src/nbi/tests/test_camara_qod_profile.py create mode 100644 src/nbi/tests/test_camara_qos_service.py diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index 72cfde514..bf1b427a0 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -41,7 +41,7 @@ spec: - containerPort: 8762 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" - name: IETF_NETWORK_RENDERER value: "LIBYANG" - name: WS_E2E_PORT diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml index 801607880..ebc218319 100644 --- a/manifests/qos_profileservice.yaml +++ b/manifests/qos_profileservice.yaml @@ -38,7 +38,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" - name: CRDB_DATABASE value: "tfs_qos_profile" envFrom: diff --git a/my_deploy.sh b/my_deploy.sh old mode 100755 new mode 100644 index 8d2e733d4..5da39cb85 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -29,7 +29,7 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui" #export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation" # Uncomment to activate QoS Profiles -#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" +export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" # Uncomment to activate BGP-LS Speaker #export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker" @@ -137,7 +137,7 @@ export CRDB_DATABASE="tfs" export CRDB_DEPLOY_MODE="single" # Disable flag for dropping database, if it exists. -export CRDB_DROP_DATABASE_IF_EXISTS="" +export CRDB_DROP_DATABASE_IF_EXISTS="YES" # Disable flag for re-deploying CockroachDB from scratch. export CRDB_REDEPLOY="" @@ -189,7 +189,7 @@ export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis" export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups" # Disable flag for dropping tables if they exist. -export QDB_DROP_TABLES_IF_EXIST="" +export QDB_DROP_TABLES_IF_EXIST="YES" # Disable flag for re-deploying QuestDB from scratch. export QDB_REDEPLOY="" diff --git a/src/nbi/Dockerfile b/src/nbi/Dockerfile index ec1c05485..78e1fdc8d 100644 --- a/src/nbi/Dockerfile +++ b/src/nbi/Dockerfile @@ -88,6 +88,8 @@ COPY src/slice/__init__.py slice/__init__.py COPY src/slice/client/. slice/client/ COPY src/qkd_app/__init__.py qkd_app/__init__.py COPY src/qkd_app/client/. qkd_app/client/ +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/ RUN mkdir -p /var/teraflow/tests/tools diff --git a/src/nbi/service/__main__.py b/src/nbi/service/__main__.py index fb735f8a7..c1eb08da2 100644 --- a/src/nbi/service/__main__.py +++ b/src/nbi/service/__main__.py @@ -22,6 +22,7 @@ from common.Settings import ( ) from .NbiService import NbiService from .rest_server.RestServer import RestServer +from .rest_server.nbi_plugins.camara_qod import register_camara_qod from .rest_server.nbi_plugins.etsi_bwm import register_etsi_bwm_api from .rest_server.nbi_plugins.ietf_hardware import register_ietf_hardware from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn @@ -70,6 +71,7 @@ def main(): grpc_service.start() rest_server = RestServer() + register_camara_qod(rest_server) register_etsi_bwm_api(rest_server) register_ietf_hardware(rest_server) register_ietf_l2vpn(rest_server) # Registering L2VPN entrypoint diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py new file mode 100644 index 000000000..a72de0323 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -0,0 +1,257 @@ +# Copyright 2022-2024 ETSI OSG/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 venv import logger +from flask.json import jsonify +from flask_restful import Resource, request +from enum import Enum +import grpc._channel +from qos_profile.service.database.QoSProfile import grpc_message_to_qos_table_data +from qos_profile.tests.test_crud import create_qos_profile_from_json,test_update_qos_profile +from qos_profile.client.QoSProfileClient import QoSProfileClient +from werkzeug.exceptions import UnsupportedMediaType +from common.proto.context_pb2 import QoSProfile, QoSProfileId, Uuid, QoSProfileValueUnitPair,Empty +from common.proto.qos_profile_pb2 import QoSProfile, QoDConstraintsRequest +from context.client.ContextClient import ContextClient +from service.client.ServiceClient import ServiceClient +from typing import Dict +from uuid import uuid4 +import grpc, logging + + +LOGGER = logging.getLogger(__name__) + + +#Initiate the QoSProfileClient +class _Resource(Resource): + def __init__(self) -> None: + super().__init__() + self.qos_profile_client = QoSProfileClient() + self.client = ContextClient() + self.service_client = ServiceClient() + +#ProfileList Endpoint for posting +class ProfileList(_Resource): + def post(self): + if not request.is_json: + return {"message": "JSON payload is required to proceed"}, 415 + request_data: Dict = request.get_json() #get the json from the test function + request_data['qos_profile_id']=str(uuid4()) # Create qos ID randomly using uuid4 + # JSON TO GRPC to store the data in the grpc server + try: + qos_profile = create_qos_profile_from_json(request_data) + except Exception as e: + LOGGER.info(e) # track if there is an error + raise e + # Send to gRPC server using CreateQosProfile done by Shayan + try: + qos_profile_created = self.qos_profile_client.CreateQoSProfile(qos_profile) + except Exception as e: + LOGGER.info(e) + raise e + #gRPC message back to JSON using the helper function created by shayan + qos_profile_data = grpc_message_to_qos_table_data(qos_profile_created) + LOGGER.info(f'qos_profile_data{qos_profile_data}') + return jsonify(qos_profile_data) + + def get(self): + qos_profiles = self.qos_profile_client.GetQoSProfiles(Empty()) #get all of type empty and defined in .proto file () + qos_profile_list = [] #since it iterates over QoSProfile , create a list to store all QOS profiles + for qos_profile in qos_profiles: + LOGGER.info('qos_profiles = {:s}'.format(str(qos_profiles))) + qos_profile_data = grpc_message_to_qos_table_data(qos_profile) #transform to json + qos_profile_list.append(qos_profile_data) # append to the list + + return jsonify(qos_profile_list) + +class ProfileListCons(_Resource): + def get(self): + qos_profile_id = request.args.get("qos_profile_id") + start_timestamp = request.args.get("start_timestamp", type=float) + duration = request.args.get("duration", type=int) + + qos_constraints_request = QoDConstraintsRequest( + qos_profile_id=qos_profile_id, + start_timestamp=start_timestamp, + duration=duration + ) + try: + qos_profiles = self.qos_profile_client.GetConstraintListFromQoSProfile(qos_constraints_request) + qos_profile_list = [ + grpc_message_to_qos_table_data(profile) for profile in qos_profiles + ] + return jsonify(qos_profile_list) + except grpc._channel._InactiveRpcError as exc: + LOGGER.error(f"gRPC error while fetching constraints: {exc}") + return {"error": "Internal Server Error"}, 500 + except Exception as e: + LOGGER.error(f"Error while fetching constraints: {e}") + return {"error": "Internal Server Error"}, 500 + +#getting,updating,deleting using the qos profile id +class ProfileDetail(_Resource): + def get(self, qos_profile_id): + id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) #this is because we want to GetQOSProfile which takes QOSProfileID + #or + #id=QoSProfileId() + #id.qos_profile_id.uuid=qos_profile_id + + #The QoSProfileID is message qod_profile_id of type Uuid + # Uuid is a message uuid of type qos_profile_id which is a string + try: + qos_profile = self.qos_profile_client.GetQoSProfile(id) #get the qosprofile from grpc server according to ID + qos_profile_data = grpc_message_to_qos_table_data(qos_profile) # grpc to json agian to view it on http + return jsonify(qos_profile_data) + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + LOGGER.warning(f"QoSProfile not found: {qos_profile_id}") + return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 + LOGGER.error(f"gRPC error while fetching QoSProfile: {exc}") + return {"error": "Internal Server Error"}, 500 + except Exception as e: + LOGGER.error(f"Error while fetching QoSProfile: {e}") + return {"error": "Internal Server Error"}, 500 + + + def put(self, qos_profile_id): + try: + request_data = request.get_json() # get the json to do the update + if 'qos_profile_id' not in request_data: #ensuring the update on qos profile id + request_data['qos_profile_id'] = qos_profile_id # ID to be updated + qos_profile = create_qos_profile_from_json(request_data) # Transform it again to grpc + qos_profile_updated = self.qos_profile_client.UpdateQoSProfile(qos_profile) # update the profile in the grpc server + return grpc_message_to_qos_table_data(qos_profile_updated), 200 + except KeyError as e: + LOGGER.error(f"Missing required key: {e}") + return {"error": f"Missing required key: {str(e)}"}, 400 + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + LOGGER.warning(f"QoSProfile not found for update: {qos_profile_id}") + return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 + LOGGER.error(f"gRPC error while updating QoSProfile: {exc}") + return {"error": "Internal Server Error"}, 500 + except Exception as e: + LOGGER.error(f"Error in PUT /profiles/{qos_profile_id}: {e}") + return {"error": "Internal Server Error"}, 500 + + def delete(self, qos_profile_id): + id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) # get id to delete accordingly + try: + qos_profile = self.qos_profile_client.DeleteQoSProfile(id) + qos_profile_data = grpc_message_to_qos_table_data(qos_profile) + return jsonify(qos_profile_data) + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + LOGGER.warning(f"QoSProfile not found for deletion: {qos_profile_id}") + return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 + LOGGER.error(f"gRPC error while deleting QoSProfile: {exc}") + return {"error": "Internal Server Error"}, 500 + except Exception as e: + LOGGER.error(f"Error in DELETE /profiles/{qos_profile_id}: {e}") + return {"error": "Internal Server Error"}, 500 + + +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, QOD_2_service, service_2_qod +) + +LOGGER = logging.getLogger(__name__) +class _Resource(Resource): + def __init__(self) -> None: + super().__init__() + self.client = ContextClient() + self.service_client = ServiceClient() + +class qodinfo(_Resource): + def post(self): + if not request.is_json: + return (jsonify({'error': 'Unsupported Media Type', 'message': 'JSON payload is required'}), 415) + request_data: Dict = request.get_json() + qos_profile_id = request_data.get('qos_profile_id') + qos_session_id = request_data.get('qos_session_id') + LOGGER.info(f'qos_profile_id:{qos_profile_id}') + if not qos_profile_id: + return jsonify({'error': 'qos_profile_id is required'}), 400 + if qos_session_id: + return jsonify({'error': 'qos_session_id is not allowed in creation'}), 400 + service = QOD_2_service(self.client, request_data,qos_profile_id) + stripped_service = copy.deepcopy(service) + stripped_service.ClearField('service_endpoint_ids') + stripped_service.ClearField('service_constraints') + stripped_service.ClearField('service_config') + try: + response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) + response = format_grpc_to_json(self.service_client.UpdateService(service)) + except Exception as e: # pylint: disable=broad-except + + return e + LOGGER.info(f"error related to response: {response}") + return response + + def get(self): + service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) + qod_info = [service_2_qod(service) for service in service_list.services] + LOGGER.info(f"error related to qod_info: {qod_info}") + return qod_info + +class qodinfoId(_Resource): + + def get(self, sessionId: str): + try: + service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) + return service_2_qod(service) + except grpc._channel._InactiveRpcError as exc: + if exc.code()==grpc.StatusCode.NOT_FOUND: + LOGGER.warning(f"Qod Session not found: {sessionId}") + return {"error": f"Qod Session {sessionId} not found"}, 404 + + def put(self, sessionId: str): + try: + request_data: Dict = request.get_json() + session_id = request_data.get('session_id') + if not session_id: + return jsonify({'error': 'sessionId is required'}), 400 + qos_profile_id = request_data.get('qos_profile_id') + if not qos_profile_id: + return jsonify({'error': 'qos_profile_id is required'}), 400 + service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) + updated_service = self.service_client.UpdateService(service) + qod_response = service_2_qod(updated_service) + return qod_response, 200 + + except KeyError as e: + LOGGER.error(f"Missing required key: {e}") + return {"error": f"Missing required key: {str(e)}"}, 400 + + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + LOGGER.warning(f"Qod Session not found: {sessionId}") + return {"error": f"Qod Session {sessionId} not found"}, 404 + LOGGER.error(f"gRPC error while updating Qod Session: {exc}") + return {"error": "Internal Server Error"}, 500 + except Exception as e: + LOGGER.error(f"Error in PUT /sessions/{sessionId}: {e}") + return {"error": "Internal Server Error"}, 500 + + def delete(self, sessionId: str): + self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) + return{"Session Deleted"} + + diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py new file mode 100644 index 000000000..043e5d92a --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py @@ -0,0 +1,195 @@ +# Copyright 2022-2024 ETSI OSG/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, re, time +from decimal import ROUND_HALF_EVEN, Decimal +from flask.json import jsonify +from common.proto.context_pb2 import ( + ContextId, Empty, EndPointId, ServiceId, ServiceStatusEnum, ServiceTypeEnum, + Service, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom, + ConfigActionEnum,QoSProfile +) +from common.tools.grpc.ConfigRules import update_config_rule_custom +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 +from uuid import uuid4 +from nbi.service.rest_server.nbi_plugins.ietf_network.bindings.networks import network +from qos_profile.client.QoSProfileClient import QoSProfileClient +#from context.service.database.QoSProfile import grpc_message_to_qos_table_data +from common.proto.context_pb2 import QoSProfile, QoSProfileId, Uuid, QoSProfileValueUnitPair,Empty,ServiceId +import logging +import grpc +from netaddr import IPAddress, IPNetwork + +LOGGER = logging.getLogger(__name__) + +ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' +DEVICE_SETTINGS_KEY = '/device[{:s}]/settings' +RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') +MEC_CONSIDERED_FIELDS = ['device', 'applicationServer', 'qosProfile', 'sessionId', 'duration', 'startedAt', 'expiresAt', 'qosStatus'] + + +def __init__(self) -> None: + super().__init__() + self.qos_profile_client = QoSProfileClient() + +def ip_withoutsubnet(ip_withsubnet,neededip): + network=IPNetwork(ip_withsubnet) + return IPAddress(neededip) in network + + +def QOD_2_service(client,qod_info: dict,qos_profile_id) -> Service: + service = Service() + service_config_rules = service.service_config.config_rules + + request_cr_key = '/request' + request_cr_value = {k: qod_info[k] for k in MEC_CONSIDERED_FIELDS if k in qod_info} + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule_custom = ConfigRule_Custom() + config_rule_custom.resource_key = request_cr_key + config_rule_custom.resource_value = json.dumps(request_cr_value) + config_rule.custom.CopyFrom(config_rule_custom) + service_config_rules.append(config_rule) + + if 'device' in qod_info and 'applicationServer' in qod_info: + a_ip = qod_info['device'].get('ipv4Address') + z_ip = qod_info['applicationServer'].get('ipv4Address') + + LOGGER.info('a_ip = {:s}'.format(str(a_ip))) + LOGGER.info('z_ip = {:s}'.format(str(z_ip))) + + if a_ip and z_ip: + devices = client.ListDevices(Empty()).devices + #LOGGER.info('devices = {:s}'.format(str(devices))) + + ip_interface_name_dict = {} + + for device in devices: + #LOGGER.info('device..uuid = {:s}'.format(str(device.device_id.device_uuid.uuid))) + #LOGGER.info('device..name = {:s}'.format(str(device.name))) + device_endpoint_uuids = {ep.name: ep.endpoint_id.endpoint_uuid.uuid for ep in device.device_endpoints} + #LOGGER.info('device_endpoint_uuids = {:s}'.format(str(device_endpoint_uuids))) + + for cr in device.device_config.config_rules: + if cr.WhichOneof('config_rule') != 'custom': + continue + #LOGGER.info('cr = {:s}'.format(str(cr))) + match_subif = RE_CONFIG_RULE_IF_SUBIF.match(cr.custom.resource_key) + if not match_subif: + continue + address_ip =json.loads(cr.custom.resource_value).get('address_ip') + LOGGER.info('cr..address_ip = {:s}'.format(str(address_ip))) + short_port_name = match_subif.groups()[0] + LOGGER.info('short_port_name = {:s}'.format(str(short_port_name))) + + ip_interface_name_dict[address_ip] = short_port_name + + if not (ip_withoutsubnet(a_ip, address_ip) or ip_withoutsubnet(z_ip, address_ip)): + continue + ep_id = EndPointId() + ep_id.endpoint_uuid.uuid = device_endpoint_uuids.get(short_port_name , '') + ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid + service.service_endpoint_ids.append(ep_id) + LOGGER.info(f"the ip address{ep_id}") + + #LOGGER.info('ip_interface_name_dict = {:s}'.format(str(ip_interface_name_dict))) + + settings_cr_key = '/settings' + settings_cr_value = {} + update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + + qod_info["sessionID"]=str(uuid4()) + qod_info["context"]='admin' + service.service_id.service_uuid.uuid = qod_info['sessionID'] + service.service_id.context_id.context_uuid.uuid = qod_info["context"] + #service.service_constraints.CopyFrom() = qod_info['contraints'] + #LOGGER.info(f'this is the error: {qod_info["context"]}') + + + try: + id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + qos_profile = client.GetQoSProfile(id) + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 + + if qos_profile.qos_profile_id: + qos_profile_id = qod_info.get('qos_profile_id') + service.name = qod_info.get('QosProfileId', qos_profile_id) + #qos_profile = QoSProfile() + #qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] + + + #if 'qosProfile' in qos_profile_list: + # qos_profile = QoSProfile() + # qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] + + + + return service + + + +def service_2_qod(service: Service) -> dict: + response = {} + for config_rule in service.service_config.config_rules: + resource_value_json = json.loads(config_rule.custom.resource_value) + LOGGER.info(f"the resource value contains:{resource_value_json}") + if config_rule.custom.resource_key != '/request': + continue + if 'device' in resource_value_json and 'ipv4Address' in resource_value_json['device']: + response['device'] = { + 'ipv4Address': resource_value_json['device']['ipv4Address'] + } + + if 'applicationServer' in resource_value_json and 'ipv4Address' in resource_value_json['applicationServer']: + response['applicationServer'] = { + 'ipv4Address': resource_value_json['applicationServer']['ipv4Address'] + } + + if service.name: + response['qos_profile_id'] = service.name + + if service.service_id: + response['sessionId'] = service.service_id.service_uuid.uuid + + if service.timestamp: + response['duration'] = service.timestamp.timestamp + LOGGER.info(f"time stamp contains{response['duration']}") + + current_time = time.time() + response['startedAt'] = int(current_time) + response['expiresAt'] = int(current_time + response['duration']) + +# unixtime = time.time() +# response['timeStamp'] = { +# "seconds": int(unixtime), +# "nanoseconds": int(unixtime % 1 * 1e9) +# } + + return response + + +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/rest_server/nbi_plugins/camara_qod/__init__.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py new file mode 100644 index 000000000..9b19a1f9e --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2022-2024 ETSI OSG/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.rest_server.RestServer import RestServer +from .Resources import ProfileList, ProfileDetail, qodinfo, qodinfoId, ProfileListCons + +URL_PREFIX = '/camara/qod/v0' + +# Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type. +RESOURCES = [ + # (endpoint_name, resource_class, resource_url) + # TODO: Add appropriate endpoints + ('camara.qod_session_info', qodinfo, '/sessions'), + ('camara.qod_info_session_id', qodinfoId, '/sessions/'), + ('camara.qod.profile_list',ProfileList,'/profiles'), + ('camara.qod.profile_detail',ProfileDetail,'/profiles/'), + ('camara.qod.profile_all',ProfileListCons,'/profiles/constraints'), + #('camara.qod.profile_delete_by_name',Profile_delete_by_name,'/profiles/delete_by_name/'), + #('camara.qod.profile_delete_all',Delete_all_profile,'/profiles/delete_all/'), + +] + +def register_camara_qod(rest_server : RestServer): + for endpoint_name, resource_class, resource_url in RESOURCES: + rest_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name) diff --git a/src/nbi/tests/test_camara_qod_profile.py b/src/nbi/tests/test_camara_qod_profile.py new file mode 100644 index 000000000..7e2eebe79 --- /dev/null +++ b/src/nbi/tests/test_camara_qod_profile.py @@ -0,0 +1,226 @@ +# Copyright 2022-2024 ETSI OSG/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 +from flask import jsonify +import requests + + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger() +BASE_URL = 'http://10.1.7.197/camara/qod/v0' + +def test_create_profile(): + BASE_URL = 'http://10.1.7.197/camara/qod/v0' + qos_profile_data={ + "name": "QCI_2_voice", + "description": "QoS profile for video streaming", + "status": "ACTIVE", + "targetMinUpstreamRate": { + "value": 10, + "unit": "bps" + }, + "maxUpstreamRate": { + "value": 10, + "unit": "bps" + }, + "maxUpstreamBurstRate": { + "value": 10, + "unit": "bps" + }, + "targetMinDownstreamRate": { + "value": 10, + "unit": "bps" + }, + "maxDownstreamRate": { + "value": 10, + "unit": "bps" + }, + "maxDownstreamBurstRate": { + "value": 10, + "unit": "bps" + }, + "minDuration": { + "value": 12, + "unit": "Minutes" + }, + "maxDuration": { + "value": 12, + "unit": "Minutes" + }, + "priority": 20, + "packetDelayBudget": { + "value": 12, + "unit": "Minutes" + }, + "jitter": { + "value": 12, + "unit": "Minutes" + }, + "packetErrorLossRate": 3 + } + + post_response = requests.post(f'{BASE_URL}/profiles', json=qos_profile_data).json() + id=post_response['qos_profile_id'] + get_response = requests.get(f'{BASE_URL}/profiles/{id}').json() + assert post_response['qos_profile_id'] == get_response['qos_profile_id'] + assert post_response['jitter'] == get_response['jitter'] + assert post_response['maxDownstreamBurstRate'] == get_response['maxDownstreamBurstRate'] + assert post_response['maxDownstreamRate'] == get_response['maxDownstreamRate'] + assert post_response['maxUpstreamBurstRate'] == get_response['maxUpstreamBurstRate'] + assert post_response['maxUpstreamRate'] == get_response['maxUpstreamRate'] + assert post_response['minDuration'] == get_response['minDuration'] + assert post_response['name'] == get_response['name'] + assert post_response['packetDelayBudget'] == get_response['packetDelayBudget'] + assert post_response['packetErrorLossRate'] == get_response['packetErrorLossRate'] + assert post_response['priority'] == get_response['priority'] + assert post_response['status'] == get_response['status'] + assert post_response['targetMinDownstreamRate'] == get_response['targetMinDownstreamRate'] + assert post_response['targetMinUpstreamRate'] == get_response['targetMinUpstreamRate'] + #assert response.status_code == 200, f"Failed to retrieve profile with status code {response.status_code}" + +#def test_update_profile(): +# qos_profile_id = '1b4689d8-02a4-4a6c-bd0a-18ffecc1a336' +# qos_profile_data = { +# "qos_profile_id": "1b4689d8-02a4-4a6c-bd0a-18ffecc1a336", +# "name": "Updated Name", +# "description": "Updated Description", +# "status": "ACTIVE", +# "targetMinUpstreamRate": { +# "value": 20, +# "unit": "bps" +# }, +# "maxUpstreamRate": { +# "value": 50, +# "unit": "bps" +# }, +# "maxUpstreamBurstRate": { +# "value": 60, +# "unit": "bps" +# }, +# "targetMinDownstreamRate": { +# "value": 30, +# "unit": "bps" +# }, +# "maxDownstreamRate": { +# "value": 100, +# "unit": "bps" +# }, +# "maxDownstreamBurstRate": { +# "value": 70, +# "unit": "bps" +# }, +# "minDuration": { +# "value": 15, +# "unit": "Minutes" +# }, +# "maxDuration": { +# "value": 25, +# "unit": "Minutes" +# }, +# "priority": 15, +# "packetDelayBudget": { +# "value": 10, +# "unit": "Minutes" +# }, +# "jitter": { +# "value": 10, +# "unit": "Minutes" +# }, +# "packetErrorLossRate": 1 +#} +# +# response = requests.put(f'{BASE_URL}/profiles/{qos_profile_id}', json=qos_profile_data) +# +#def test_delete_profile_by_id(): +# qos_profile_id = 'adcbc52e-85e1-42e2-aa33-b6d022798fb3' +# response = requests.delete(f'{BASE_URL}/profiles/{qos_profile_id}') + +import logging +from flask import jsonify +import requests + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger() + +# Define the base URL for the API +BASE_URL = 'http://10.1.7.197/camara/qod/v0' + +def test_create_profile(): + # Define the QoS profile data + qos_profile_data = { + "name": "QCI_2_voice", + "description": "QoS profile for video streaming", + "status": "ACTIVE", + "targetMinUpstreamRate": {"value": 10, "unit": "bps"}, + "maxUpstreamRate": {"value": 10, "unit": "bps"}, + "maxUpstreamBurstRate": {"value": 10, "unit": "bps"}, + "targetMinDownstreamRate": {"value": 10, "unit": "bps"}, + "maxDownstreamRate": {"value": 10, "unit": "bps"}, + "maxDownstreamBurstRate": {"value": 10, "unit": "bps"}, + "minDuration": {"value": 12, "unit": "Minutes"}, + "maxDuration": {"value": 12, "unit": "Minutes"}, + "priority": 20, + "packetDelayBudget": {"value": 12, "unit": "Minutes"}, + "jitter": {"value": 12, "unit": "Minutes"}, + "packetErrorLossRate": 3 + } + + # Test profile creation + post_response = requests.post(f'{BASE_URL}/profiles', json=qos_profile_data).json() + id = post_response['qos_profile_id'] + + # Test profile retrieval + get_response = requests.get(f'{BASE_URL}/profiles/{id}').json() + + # Assertions to check if post and get responses match + assert post_response['qos_profile_id'] == get_response['qos_profile_id'] + assert post_response['jitter'] == get_response['jitter'] + assert post_response['maxDownstreamBurstRate'] == get_response['maxDownstreamBurstRate'] + assert post_response['maxDownstreamRate'] == get_response['maxDownstreamRate'] + assert post_response['maxUpstreamBurstRate'] == get_response['maxUpstreamBurstRate'] + assert post_response['maxUpstreamRate'] == get_response['maxUpstreamRate'] + assert post_response['minDuration'] == get_response['minDuration'] + assert post_response['name'] == get_response['name'] + assert post_response['packetDelayBudget'] == get_response['packetDelayBudget'] + assert post_response['packetErrorLossRate'] == get_response['packetErrorLossRate'] + assert post_response['priority'] == get_response['priority'] + assert post_response['status'] == get_response['status'] + assert post_response['targetMinDownstreamRate'] == get_response['targetMinDownstreamRate'] + assert post_response['targetMinUpstreamRate'] == get_response['targetMinUpstreamRate'] + +def test_get_constraints(): + # Replace with actual qos_profile_id you want to test with + qos_profile_id = "contraints" + start_timestamp = 1726063284.25332 + duration = 86400 + + # Send GET request to fetch constraints + response = requests.get(f'{BASE_URL}/profiles/constraints', params={ + "qos_profile_id": qos_profile_id, + "start_timestamp": start_timestamp, + "duration": duration + }) + + # Convert response to JSON and add error checking + if response.status_code == 200: + constraints = response.json() + LOGGER.debug(f"Constraints retrieved: {constraints}") + + # Additional assertions for constraints + assert len(constraints) > 0, "Expected at least one constraint" + first_constraint = constraints[0] + assert "constraint_type" in first_constraint, "Constraint type missing in response" + else: + LOGGER.error(f"Failed to fetch constraints: Status code {response.status_code}") diff --git a/src/nbi/tests/test_camara_qos_service.py b/src/nbi/tests/test_camara_qos_service.py new file mode 100644 index 000000000..247268512 --- /dev/null +++ b/src/nbi/tests/test_camara_qos_service.py @@ -0,0 +1,52 @@ +# Copyright 2022-2024 ETSI OSG/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 +from flask import jsonify +import requests + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger() +BASE_URL = 'http://10.1.7.197/camara/qod/v0' + +#def test_create_SESSION(): +# BASE_URL = 'http://10.1.7.197/camara/qod/v0' +# service_data={ +# "device": +# {"ipv4Address":"84.75.11.12/25" }, +# "applicationServer": { +# "ipv4Address": "192.168.0.1/26", +# }, +# "duration":100.00, +# "qos_profile_id": "6f39d0ae-f1a4-4e05-ad3c-bc77bdbb7fd0", +# } +# post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() +# #id=post_response['sessionID'] +# #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() +# get_response = requests.get(f'{BASE_URL}/sessions').json() +#def test_delete_session_by_id(): +# session_id = '' +# response = requests.delete(f'{BASE_URL}/sessions/{session_id}') + +#def test_update_session_by_id(): +# session_id='f192a586-4869-4d28-8793-01478fa149041' +# session_data={"session_id":'f192a586-4869-4d28-8793-01478fa14904', +# "device": +# {"ipv4Address":"84.75.11.12/25" }, +# "applicationServer": { +# "ipv4Address": "192.168.0.1/26", +# }, +# "duration":200.00, +# "qos_profile_id": "6f39d0ae-f1a4-4e05-ad3c-bc77bdbb7fd0"} +# put_response=requests.put(f'{BASE_URL}/sessions/{session_id}',json=session_data).json() \ No newline at end of file -- GitLab From 6d2e3f91f494b44647b457935c7529041ed5df6a Mon Sep 17 00:00:00 2001 From: rahhal Date: Tue, 15 Oct 2024 15:26:08 +0000 Subject: [PATCH 02/19] Updated First verison of CAMARA QoD NBI connector --- manifests/nginx_ingress_http.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/manifests/nginx_ingress_http.yaml b/manifests/nginx_ingress_http.yaml index 619d85f7a..8f96f000e 100644 --- a/manifests/nginx_ingress_http.yaml +++ b/manifests/nginx_ingress_http.yaml @@ -69,3 +69,10 @@ spec: name: nbiservice port: number: 8080 + - path: /()(camara/.*) + pathType: Prefix + backend: + service: + name: nbiservice + port: + number: 8080 -- GitLab From c6b134a641b9138e284bdfaccdc1c957901355b8 Mon Sep 17 00:00:00 2001 From: rahhal Date: Fri, 15 Nov 2024 14:15:39 +0000 Subject: [PATCH 03/19] Updated version of CAMARA NBI --- scripts/run_tests_locally-nbi-camara-qod.sh | 30 +++++ .../nbi_plugins/camara_qod/Resources.py | 82 ++++------- .../nbi_plugins/camara_qod/Tools.py | 127 ++++++++++++------ .../nbi_plugins/camara_qod/__init__.py | 6 +- src/nbi/tests/test_camara_qod_profile.py | 93 ++----------- src/nbi/tests/test_camara_qos_service.py | 49 +++---- 6 files changed, 178 insertions(+), 209 deletions(-) create mode 100755 scripts/run_tests_locally-nbi-camara-qod.sh diff --git a/scripts/run_tests_locally-nbi-camara-qod.sh b/scripts/run_tests_locally-nbi-camara-qod.sh new file mode 100755 index 000000000..c37a95984 --- /dev/null +++ b/scripts/run_tests_locally-nbi-camara-qod.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Copyright 2022-2024 ETSI OSG/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. + +PROJECTDIR=`pwd` + +cd $PROJECTDIR/src +RCFILE=$PROJECTDIR/coverage/.coveragerc +COVERAGEFILE=$PROJECTDIR/coverage/.coverage + +# Destroy old coverage file and configure the correct folder on the .coveragerc file +rm -f $COVERAGEFILE +cat $PROJECTDIR/coverage/.coveragerc.template | sed s+~/tfs-ctrl+$PROJECTDIR+g > $RCFILE + + +# Run unitary tests and analyze coverage of code at same time +# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 +coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ + nbi/tests/test_camara_qod_profile.py nbi/tests/test_camara_qos_service.py diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index a72de0323..b208b561f 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -11,27 +11,30 @@ # 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 venv import logger from flask.json import jsonify from flask_restful import Resource, request from enum import Enum import grpc._channel -from qos_profile.service.database.QoSProfile import grpc_message_to_qos_table_data -from qos_profile.tests.test_crud import create_qos_profile_from_json,test_update_qos_profile from qos_profile.client.QoSProfileClient import QoSProfileClient from werkzeug.exceptions import UnsupportedMediaType -from common.proto.context_pb2 import QoSProfile, QoSProfileId, Uuid, QoSProfileValueUnitPair,Empty -from common.proto.qos_profile_pb2 import QoSProfile, QoDConstraintsRequest -from context.client.ContextClient import ContextClient -from service.client.ServiceClient import ServiceClient +from common.proto.context_pb2 import QoSProfileId, Uuid,Empty +from common.proto.qos_profile_pb2 import QoDConstraintsRequest from typing import Dict from uuid import uuid4 import grpc, logging +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, QOD_2_service, service_2_qod,grpc_message_to_qos_table_data,create_qos_profile_from_json +) -LOGGER = logging.getLogger(__name__) - - +LOGGER = logging.getLogger(__name__) #Initiate the QoSProfileClient class _Resource(Resource): def __init__(self) -> None: @@ -74,30 +77,6 @@ class ProfileList(_Resource): return jsonify(qos_profile_list) -class ProfileListCons(_Resource): - def get(self): - qos_profile_id = request.args.get("qos_profile_id") - start_timestamp = request.args.get("start_timestamp", type=float) - duration = request.args.get("duration", type=int) - - qos_constraints_request = QoDConstraintsRequest( - qos_profile_id=qos_profile_id, - start_timestamp=start_timestamp, - duration=duration - ) - try: - qos_profiles = self.qos_profile_client.GetConstraintListFromQoSProfile(qos_constraints_request) - qos_profile_list = [ - grpc_message_to_qos_table_data(profile) for profile in qos_profiles - ] - return jsonify(qos_profile_list) - except grpc._channel._InactiveRpcError as exc: - LOGGER.error(f"gRPC error while fetching constraints: {exc}") - return {"error": "Internal Server Error"}, 500 - except Exception as e: - LOGGER.error(f"Error while fetching constraints: {e}") - return {"error": "Internal Server Error"}, 500 - #getting,updating,deleting using the qos profile id class ProfileDetail(_Resource): def get(self, qos_profile_id): @@ -160,25 +139,8 @@ class ProfileDetail(_Resource): LOGGER.error(f"Error in DELETE /profiles/{qos_profile_id}: {e}") return {"error": "Internal Server Error"}, 500 - -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, QOD_2_service, service_2_qod -) - +###SESSION########################################################## LOGGER = logging.getLogger(__name__) -class _Resource(Resource): - def __init__(self) -> None: - super().__init__() - self.client = ContextClient() - self.service_client = ServiceClient() - class qodinfo(_Resource): def post(self): if not request.is_json: @@ -186,12 +148,13 @@ class qodinfo(_Resource): request_data: Dict = request.get_json() qos_profile_id = request_data.get('qos_profile_id') qos_session_id = request_data.get('qos_session_id') + duration = request_data.get('duration') LOGGER.info(f'qos_profile_id:{qos_profile_id}') if not qos_profile_id: return jsonify({'error': 'qos_profile_id is required'}), 400 if qos_session_id: return jsonify({'error': 'qos_session_id is not allowed in creation'}), 400 - service = QOD_2_service(self.client, request_data,qos_profile_id) + service = QOD_2_service(self.client, request_data,qos_profile_id,duration) stripped_service = copy.deepcopy(service) stripped_service.ClearField('service_endpoint_ids') stripped_service.ClearField('service_constraints') @@ -206,8 +169,8 @@ class qodinfo(_Resource): return response def get(self): - service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) - qod_info = [service_2_qod(service) for service in service_list.services] + service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) #return context id as json + qod_info = [service_2_qod(service) for service in service_list.services] #iterating over service list LOGGER.info(f"error related to qod_info: {qod_info}") return qod_info @@ -231,7 +194,14 @@ class qodinfoId(_Resource): qos_profile_id = request_data.get('qos_profile_id') if not qos_profile_id: return jsonify({'error': 'qos_profile_id is required'}), 400 - service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) + duration = request_data.get('duration') + service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) #to get service we should have the context and the session id + if qos_profile_id: + service.name = qos_profile_id # if we provide a new qos profile , update the service name with new qos_profile_id + if duration: + for constraint in service.service_constraints: + if constraint.WhichOneof('constraint') == 'schedule': + constraint.schedule.duration_days = duration updated_service = self.service_client.UpdateService(service) qod_response = service_2_qod(updated_service) return qod_response, 200 diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py index 043e5d92a..1b246a79d 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py @@ -13,13 +13,11 @@ # limitations under the License. import json, logging, re, time -from decimal import ROUND_HALF_EVEN, Decimal from flask.json import jsonify from common.proto.context_pb2 import ( ContextId, Empty, EndPointId, ServiceId, ServiceStatusEnum, ServiceTypeEnum, - Service, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom, - ConfigActionEnum,QoSProfile -) + Service,ConfigRule, ConfigRule_Custom, + ConfigActionEnum) from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.grpc.Tools import grpc_message_to_json from common.tools.object_factory.Context import json_context_id @@ -27,10 +25,11 @@ from common.tools.object_factory.Service import json_service_id from uuid import uuid4 from nbi.service.rest_server.nbi_plugins.ietf_network.bindings.networks import network from qos_profile.client.QoSProfileClient import QoSProfileClient -#from context.service.database.QoSProfile import grpc_message_to_qos_table_data -from common.proto.context_pb2 import QoSProfile, QoSProfileId, Uuid, QoSProfileValueUnitPair,Empty,ServiceId +from context.client.ContextClient import ContextClient +from common.proto.context_pb2 import QoSProfileId, Uuid,Empty,ServiceId +from common.proto.context_pb2 import Uuid, QoSProfileId +from common.proto.qos_profile_pb2 import QoSProfileValueUnitPair, QoSProfile,QoDConstraintsRequest import logging -import grpc from netaddr import IPAddress, IPNetwork LOGGER = logging.getLogger(__name__) @@ -40,17 +39,60 @@ DEVICE_SETTINGS_KEY = '/device[{:s}]/settings' RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') MEC_CONSIDERED_FIELDS = ['device', 'applicationServer', 'qosProfile', 'sessionId', 'duration', 'startedAt', 'expiresAt', 'qosStatus'] - def __init__(self) -> None: super().__init__() self.qos_profile_client = QoSProfileClient() + self.client = ContextClient() + +def grpc_message_to_qos_table_data(message: QoSProfile) -> dict: + return { + 'qos_profile_id' : message.qos_profile_id.qos_profile_id.uuid, + 'name' : message.name, + 'description' : message.description, + 'status' : message.status, + 'targetMinUpstreamRate' : grpc_message_to_json(message.targetMinUpstreamRate), + 'maxUpstreamRate' : grpc_message_to_json(message.maxUpstreamRate), + 'maxUpstreamBurstRate' : grpc_message_to_json(message.maxUpstreamBurstRate), + 'targetMinDownstreamRate' : grpc_message_to_json(message.targetMinDownstreamRate), + 'maxDownstreamRate' : grpc_message_to_json(message.maxDownstreamRate), + 'maxDownstreamBurstRate' : grpc_message_to_json(message.maxDownstreamBurstRate), + 'minDuration' : grpc_message_to_json(message.minDuration), + 'maxDuration' : grpc_message_to_json(message.maxDuration), + 'priority' : message.priority, + 'packetDelayBudget' : grpc_message_to_json(message.packetDelayBudget), + 'jitter' : grpc_message_to_json(message.jitter), + 'packetErrorLossRate' : message.packetErrorLossRate, + } + +def create_qos_profile_from_json(qos_profile_data: dict) -> QoSProfile: + def create_QoSProfileValueUnitPair(data) -> QoSProfileValueUnitPair: + return QoSProfileValueUnitPair(value=data['value'], unit=data['unit']) + qos_profile = QoSProfile() + qos_profile.qos_profile_id.CopyFrom(QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_data['qos_profile_id']))) + qos_profile.name = qos_profile_data['name'] + qos_profile.description = qos_profile_data['description'] + qos_profile.status = qos_profile_data['status'] + qos_profile.targetMinUpstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['targetMinUpstreamRate'])) + qos_profile.maxUpstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxUpstreamRate'])) + qos_profile.maxUpstreamBurstRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxUpstreamBurstRate'])) + qos_profile.targetMinDownstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['targetMinDownstreamRate'])) + qos_profile.maxDownstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDownstreamRate'])) + qos_profile.maxDownstreamBurstRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDownstreamBurstRate'])) + qos_profile.minDuration.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['minDuration'])) + qos_profile.maxDuration.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDuration'])) + qos_profile.priority = qos_profile_data['priority'] + qos_profile.packetDelayBudget.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['packetDelayBudget'])) + qos_profile.jitter.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['jitter'])) + qos_profile.packetErrorLossRate = qos_profile_data['packetErrorLossRate'] + return qos_profile def ip_withoutsubnet(ip_withsubnet,neededip): network=IPNetwork(ip_withsubnet) return IPAddress(neededip) in network -def QOD_2_service(client,qod_info: dict,qos_profile_id) -> Service: +def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: + qos_profile_client = QoSProfileClient() service = Service() service_config_rules = service.service_config.config_rules @@ -112,34 +154,43 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id) -> Service: update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM - qod_info["sessionID"]=str(uuid4()) qod_info["context"]='admin' service.service_id.service_uuid.uuid = qod_info['sessionID'] service.service_id.context_id.context_uuid.uuid = qod_info["context"] - #service.service_constraints.CopyFrom() = qod_info['contraints'] - #LOGGER.info(f'this is the error: {qod_info["context"]}') - - try: - id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) - qos_profile = client.GetQoSProfile(id) - except grpc._channel._InactiveRpcError as exc: - if exc.code() == grpc.StatusCode.NOT_FOUND: - return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - - if qos_profile.qos_profile_id: - qos_profile_id = qod_info.get('qos_profile_id') - service.name = qod_info.get('QosProfileId', qos_profile_id) + #try: + # id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + # id= QoSProfileId() + # id.qos_profile_id.uuid="qos_profile_id" + # qos_profile = qos_profile_client.GetQoSProfile(id) + #except grpc._channel._InactiveRpcError as exc: + # if exc.code() == grpc.StatusCode.NOT_FOUND: + # return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 + #if qos_profile.qos_profile_id: + # qos_profile_id = qod_info.get('qos_profile_id') + service.name = qod_info.get('qos_profile_id', qos_profile_id) + current_time = time.time() + duration_days = duration # days as i saw it in the proto files + id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + request = QoDConstraintsRequest(qos_profile_id=id,start_timestamp=current_time,duration=duration_days) #defined attributes in proto file for the QoDconstraint rquest message + qos_profiles_constraint = qos_profile_client.GetConstraintListFromQoSProfile(request) + LOGGER.info(f'current time contains{current_time}') + LOGGER.info(f'current duration time contains{duration_days}') + LOGGER.info(f'id : {id}') + for cs in qos_profiles_constraint: + if cs.WhichOneof('constraint') == 'schedule' and cs.WhichOneof('constraint') == 'qos_profile': #the method of which one of + cs.schedule.start_timestamp = current_time + cs.schedule.duration_days = duration_days + cs.qos_profile.qos_profile_id=id + cs.qos_profile.qos_profile_name.CopyFrom(qos_profile_client.name) #i copied this from the qosprofile + LOGGER.info(f'the cs : {cs}') + service.service_constraints.append(cs) #qos_profile = QoSProfile() #qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] - - #if 'qosProfile' in qos_profile_list: # qos_profile = QoSProfile() # qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] - - return service @@ -167,20 +218,14 @@ def service_2_qod(service: Service) -> dict: if service.service_id: response['sessionId'] = service.service_id.service_uuid.uuid - - if service.timestamp: - response['duration'] = service.timestamp.timestamp - LOGGER.info(f"time stamp contains{response['duration']}") - - current_time = time.time() - response['startedAt'] = int(current_time) - response['expiresAt'] = int(current_time + response['duration']) - -# unixtime = time.time() -# response['timeStamp'] = { -# "seconds": int(unixtime), -# "nanoseconds": int(unixtime % 1 * 1e9) -# } + if service.service_constraints: + for constraint in service.service_constraints: + if constraint.WhichOneof('constraint') == 'schedule': + response['duration'] = float(constraint.schedule.duration_days* (86400)) + LOGGER.info(f'the duration in seconds: {response["duration"]}') + response['startedAt'] = int(constraint.schedule.start_timestamp) + response['expiresAt'] = response['startedAt'] + response['duration'] + return response diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py index 9b19a1f9e..ab4fe2bbd 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. from nbi.service.rest_server.RestServer import RestServer -from .Resources import ProfileList, ProfileDetail, qodinfo, qodinfoId, ProfileListCons +from .Resources import ProfileList, ProfileDetail, qodinfo, qodinfoId URL_PREFIX = '/camara/qod/v0' @@ -25,10 +25,6 @@ RESOURCES = [ ('camara.qod_info_session_id', qodinfoId, '/sessions/'), ('camara.qod.profile_list',ProfileList,'/profiles'), ('camara.qod.profile_detail',ProfileDetail,'/profiles/'), - ('camara.qod.profile_all',ProfileListCons,'/profiles/constraints'), - #('camara.qod.profile_delete_by_name',Profile_delete_by_name,'/profiles/delete_by_name/'), - #('camara.qod.profile_delete_all',Delete_all_profile,'/profiles/delete_all/'), - ] def register_camara_qod(rest_server : RestServer): diff --git a/src/nbi/tests/test_camara_qod_profile.py b/src/nbi/tests/test_camara_qod_profile.py index 7e2eebe79..4408305a9 100644 --- a/src/nbi/tests/test_camara_qod_profile.py +++ b/src/nbi/tests/test_camara_qod_profile.py @@ -88,14 +88,17 @@ def test_create_profile(): assert post_response['status'] == get_response['status'] assert post_response['targetMinDownstreamRate'] == get_response['targetMinDownstreamRate'] assert post_response['targetMinUpstreamRate'] == get_response['targetMinUpstreamRate'] - #assert response.status_code == 200, f"Failed to retrieve profile with status code {response.status_code}" +#assert response.status_code == 200, f"Failed to retrieve profile with status code {response.status_code}" + + + #def test_update_profile(): -# qos_profile_id = '1b4689d8-02a4-4a6c-bd0a-18ffecc1a336' +# qos_profile_id = '0898e7e8-ef15-4522-8e93-623e31c92efa' # qos_profile_data = { -# "qos_profile_id": "1b4689d8-02a4-4a6c-bd0a-18ffecc1a336", +# "qos_profile_id": "0898e7e8-ef15-4522-8e93-623e31c92efa", # "name": "Updated Name", -# "description": "Updated Description", +# "description": "NEW GAMING PROFILE", # "status": "ACTIVE", # "targetMinUpstreamRate": { # "value": 20, @@ -140,87 +143,11 @@ def test_create_profile(): # }, # "packetErrorLossRate": 1 #} -# # response = requests.put(f'{BASE_URL}/profiles/{qos_profile_id}', json=qos_profile_data) -# -#def test_delete_profile_by_id(): -# qos_profile_id = 'adcbc52e-85e1-42e2-aa33-b6d022798fb3' -# response = requests.delete(f'{BASE_URL}/profiles/{qos_profile_id}') - -import logging -from flask import jsonify -import requests - -logging.basicConfig(level=logging.DEBUG) -LOGGER = logging.getLogger() - -# Define the base URL for the API -BASE_URL = 'http://10.1.7.197/camara/qod/v0' - -def test_create_profile(): - # Define the QoS profile data - qos_profile_data = { - "name": "QCI_2_voice", - "description": "QoS profile for video streaming", - "status": "ACTIVE", - "targetMinUpstreamRate": {"value": 10, "unit": "bps"}, - "maxUpstreamRate": {"value": 10, "unit": "bps"}, - "maxUpstreamBurstRate": {"value": 10, "unit": "bps"}, - "targetMinDownstreamRate": {"value": 10, "unit": "bps"}, - "maxDownstreamRate": {"value": 10, "unit": "bps"}, - "maxDownstreamBurstRate": {"value": 10, "unit": "bps"}, - "minDuration": {"value": 12, "unit": "Minutes"}, - "maxDuration": {"value": 12, "unit": "Minutes"}, - "priority": 20, - "packetDelayBudget": {"value": 12, "unit": "Minutes"}, - "jitter": {"value": 12, "unit": "Minutes"}, - "packetErrorLossRate": 3 - } - # Test profile creation - post_response = requests.post(f'{BASE_URL}/profiles', json=qos_profile_data).json() - id = post_response['qos_profile_id'] - # Test profile retrieval - get_response = requests.get(f'{BASE_URL}/profiles/{id}').json() - - # Assertions to check if post and get responses match - assert post_response['qos_profile_id'] == get_response['qos_profile_id'] - assert post_response['jitter'] == get_response['jitter'] - assert post_response['maxDownstreamBurstRate'] == get_response['maxDownstreamBurstRate'] - assert post_response['maxDownstreamRate'] == get_response['maxDownstreamRate'] - assert post_response['maxUpstreamBurstRate'] == get_response['maxUpstreamBurstRate'] - assert post_response['maxUpstreamRate'] == get_response['maxUpstreamRate'] - assert post_response['minDuration'] == get_response['minDuration'] - assert post_response['name'] == get_response['name'] - assert post_response['packetDelayBudget'] == get_response['packetDelayBudget'] - assert post_response['packetErrorLossRate'] == get_response['packetErrorLossRate'] - assert post_response['priority'] == get_response['priority'] - assert post_response['status'] == get_response['status'] - assert post_response['targetMinDownstreamRate'] == get_response['targetMinDownstreamRate'] - assert post_response['targetMinUpstreamRate'] == get_response['targetMinUpstreamRate'] -def test_get_constraints(): - # Replace with actual qos_profile_id you want to test with - qos_profile_id = "contraints" - start_timestamp = 1726063284.25332 - duration = 86400 +# def test_delete_profile_by_id(): +# qos_profile_id = '0898e7e8-ef15-4522-8e93-623e31c92efa' +# response = requests.delete(f'{BASE_URL}/profiles/{qos_profile_id}') - # Send GET request to fetch constraints - response = requests.get(f'{BASE_URL}/profiles/constraints', params={ - "qos_profile_id": qos_profile_id, - "start_timestamp": start_timestamp, - "duration": duration - }) - - # Convert response to JSON and add error checking - if response.status_code == 200: - constraints = response.json() - LOGGER.debug(f"Constraints retrieved: {constraints}") - - # Additional assertions for constraints - assert len(constraints) > 0, "Expected at least one constraint" - first_constraint = constraints[0] - assert "constraint_type" in first_constraint, "Constraint type missing in response" - else: - LOGGER.error(f"Failed to fetch constraints: Status code {response.status_code}") diff --git a/src/nbi/tests/test_camara_qos_service.py b/src/nbi/tests/test_camara_qos_service.py index 247268512..fe4ee3f2e 100644 --- a/src/nbi/tests/test_camara_qos_service.py +++ b/src/nbi/tests/test_camara_qos_service.py @@ -19,34 +19,35 @@ import requests logging.basicConfig(level=logging.DEBUG) LOGGER = logging.getLogger() BASE_URL = 'http://10.1.7.197/camara/qod/v0' +def test_create_SESSION(): + BASE_URL = 'http://10.1.7.197/camara/qod/v0' + service_data={ + "device": + {"ipv4Address":"84.75.11.12/25" }, + "applicationServer": { + "ipv4Address": "192.168.0.1/26", + }, + "duration":10000000.00, + "qos_profile_id": "367d3e3f-96be-4391-af82-e21c042dd9bd", + } + post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() + #id=post_response['sessionID'] + #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() + get_response = requests.get(f'{BASE_URL}/sessions').json() + -#def test_create_SESSION(): -# BASE_URL = 'http://10.1.7.197/camara/qod/v0' -# service_data={ -# "device": -# {"ipv4Address":"84.75.11.12/25" }, -# "applicationServer": { -# "ipv4Address": "192.168.0.1/26", -# }, -# "duration":100.00, -# "qos_profile_id": "6f39d0ae-f1a4-4e05-ad3c-bc77bdbb7fd0", -# } -# post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() -# #id=post_response['sessionID'] -# #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() -# get_response = requests.get(f'{BASE_URL}/sessions').json() #def test_delete_session_by_id(): # session_id = '' # response = requests.delete(f'{BASE_URL}/sessions/{session_id}') #def test_update_session_by_id(): -# session_id='f192a586-4869-4d28-8793-01478fa149041' -# session_data={"session_id":'f192a586-4869-4d28-8793-01478fa14904', -# "device": -# {"ipv4Address":"84.75.11.12/25" }, -# "applicationServer": { -# "ipv4Address": "192.168.0.1/26", -# }, -# "duration":200.00, -# "qos_profile_id": "6f39d0ae-f1a4-4e05-ad3c-bc77bdbb7fd0"} +# session_id='3ac2ff12-d763-4ded-9e13-7e82709b16cd' +# session_data={"session_id":'3ac2ff12-d763-4ded-9e13-7e82709b16cd', +# "device": +# {"ipv4Address":"84.75.11.12/25" }, +# "applicationServer": { +# "ipv4Address": "192.168.0.1/26", +# }, +# "duration":2000000000.00, +# "qos_profile_id": "28499a15-c1d9-428c-9732-82859a727235"} # put_response=requests.put(f'{BASE_URL}/sessions/{session_id}',json=session_data).json() \ No newline at end of file -- GitLab From f5c5389973c679f8af7a476efdba955991f37314 Mon Sep 17 00:00:00 2001 From: Shayan Hajipour Date: Tue, 11 Feb 2025 08:12:56 +0000 Subject: [PATCH 04/19] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Shayan Hajipour --- manifests/nbiservice.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index c156ef26c..55accdc44 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -41,7 +41,7 @@ spec: - containerPort: 8762 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: IETF_NETWORK_RENDERER value: "LIBYANG" - name: WS_E2E_PORT -- GitLab From 04b8aac6ddf5e866fe2177fab8c66e7b43a2903e Mon Sep 17 00:00:00 2001 From: Shayan Hajipour Date: Tue, 11 Feb 2025 08:23:29 +0000 Subject: [PATCH 05/19] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Shayan Hajipour --- manifests/qos_profileservice.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml index b851aec4d..01b8adf76 100644 --- a/manifests/qos_profileservice.yaml +++ b/manifests/qos_profileservice.yaml @@ -38,7 +38,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: CRDB_DATABASE value: "tfs_qos_profile" envFrom: -- GitLab From eeb62a1f722ed0db6eacd8d5e2b44e551ab6345e Mon Sep 17 00:00:00 2001 From: Shayan Hajipour Date: Tue, 11 Feb 2025 08:36:26 +0000 Subject: [PATCH 06/19] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Shayan Hajipour --- src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index b208b561f..7d81db99c 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -54,7 +54,7 @@ class ProfileList(_Resource): try: qos_profile = create_qos_profile_from_json(request_data) except Exception as e: - LOGGER.info(e) # track if there is an error + LOGGER.error(e) # track if there is an error raise e # Send to gRPC server using CreateQosProfile done by Shayan try: -- GitLab From 4964ac9e13e0b6ca00f9dcf234fd922d8f6ae7cd Mon Sep 17 00:00:00 2001 From: Shayan Hajipour Date: Tue, 11 Feb 2025 09:33:56 +0000 Subject: [PATCH 07/19] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: Shayan Hajipour --- .../service/rest_server/nbi_plugins/camara_qod/Resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index 7d81db99c..d6b4ee3fc 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -163,9 +163,9 @@ class qodinfo(_Resource): response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) response = format_grpc_to_json(self.service_client.UpdateService(service)) except Exception as e: # pylint: disable=broad-except - + LOGGER.error(f"error related to response: {response}") return e - LOGGER.info(f"error related to response: {response}") + return response def get(self): -- GitLab From 0b15ad14da2cf5d4a51129a212e0756265948bc8 Mon Sep 17 00:00:00 2001 From: Mohamad Rahhal Date: Tue, 11 Feb 2025 12:28:33 +0000 Subject: [PATCH 08/19] QOS CAMARA- NBI: - Correction After Shayan's Review --- manifests/nbiservice.yaml | 2 +- manifests/qos_profileservice.yaml | 2 +- my_deploy.sh | 2 +- .../nbi_plugins/camara_qod/Resources.py | 21 +++++++------- .../nbi_plugins/camara_qod/Tools.py | 28 ++++--------------- src/nbi/tests/test_camara_qod_profile.py | 3 +- src/nbi/tests/test_camara_qos_service.py | 3 +- 7 files changed, 21 insertions(+), 40 deletions(-) diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index c156ef26c..55accdc44 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -41,7 +41,7 @@ spec: - containerPort: 8762 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: IETF_NETWORK_RENDERER value: "LIBYANG" - name: WS_E2E_PORT diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml index b851aec4d..01b8adf76 100644 --- a/manifests/qos_profileservice.yaml +++ b/manifests/qos_profileservice.yaml @@ -38,7 +38,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: CRDB_DATABASE value: "tfs_qos_profile" envFrom: diff --git a/my_deploy.sh b/my_deploy.sh index 7984f77d0..225480682 100644 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -134,7 +134,7 @@ export CRDB_PASSWORD="tfs123" export CRDB_DEPLOY_MODE="single" # Disable flag for dropping database, if it exists. -export CRDB_DROP_DATABASE_IF_EXISTS="YES" +export CRDB_DROP_DATABASE_IF_EXISTS="" # Disable flag for re-deploying CockroachDB from scratch. export CRDB_REDEPLOY="" diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index b208b561f..9e1072afe 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -54,15 +54,15 @@ class ProfileList(_Resource): try: qos_profile = create_qos_profile_from_json(request_data) except Exception as e: - LOGGER.info(e) # track if there is an error + LOGGER.error(e) # track if there is an error raise e - # Send to gRPC server using CreateQosProfile done by Shayan + # Send to gRPC server using CreateQosProfile try: qos_profile_created = self.qos_profile_client.CreateQoSProfile(qos_profile) except Exception as e: - LOGGER.info(e) + LOGGER.error(e) raise e - #gRPC message back to JSON using the helper function created by shayan + #gRPC message back to JSON using the helper function qos_profile_data = grpc_message_to_qos_table_data(qos_profile_created) LOGGER.info(f'qos_profile_data{qos_profile_data}') return jsonify(qos_profile_data) @@ -140,8 +140,7 @@ class ProfileDetail(_Resource): return {"error": "Internal Server Error"}, 500 ###SESSION########################################################## -LOGGER = logging.getLogger(__name__) -class qodinfo(_Resource): +class QodInfo(_Resource): def post(self): if not request.is_json: return (jsonify({'error': 'Unsupported Media Type', 'message': 'JSON payload is required'}), 415) @@ -163,18 +162,18 @@ class qodinfo(_Resource): response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) response = format_grpc_to_json(self.service_client.UpdateService(service)) except Exception as e: # pylint: disable=broad-except - - return e - LOGGER.info(f"error related to response: {response}") + LOGGER.error(f"Unexpected error: {str(e)}", exc_info=True) + return jsonify({'error': 'Internal Server Error', 'message': 'An unexpected error occurred'}), 500 + LOGGER.error(f"error related to response: {response}") return response def get(self): service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) #return context id as json qod_info = [service_2_qod(service) for service in service_list.services] #iterating over service list LOGGER.info(f"error related to qod_info: {qod_info}") - return qod_info + return jsonify(qod_info), 200 -class qodinfoId(_Resource): +class QodInfoID(_Resource): def get(self, sessionId: str): try: diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py index 1b246a79d..feec71cb6 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py @@ -91,7 +91,7 @@ def ip_withoutsubnet(ip_withsubnet,neededip): return IPAddress(neededip) in network -def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: +def QOD_2_service(client,qod_info: dict,qos_profile_id: int,duration: int) -> Service: qos_profile_client = QoSProfileClient() service = Service() service_config_rules = service.service_config.config_rules @@ -110,8 +110,8 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: a_ip = qod_info['device'].get('ipv4Address') z_ip = qod_info['applicationServer'].get('ipv4Address') - LOGGER.info('a_ip = {:s}'.format(str(a_ip))) - LOGGER.info('z_ip = {:s}'.format(str(z_ip))) + LOGGER.debug('a_ip = {:s}'.format(str(a_ip))) + LOGGER.debug('z_ip = {:s}'.format(str(z_ip))) if a_ip and z_ip: devices = client.ListDevices(Empty()).devices @@ -133,9 +133,9 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: if not match_subif: continue address_ip =json.loads(cr.custom.resource_value).get('address_ip') - LOGGER.info('cr..address_ip = {:s}'.format(str(address_ip))) + LOGGER.debug('cr..address_ip = {:s}'.format(str(address_ip))) short_port_name = match_subif.groups()[0] - LOGGER.info('short_port_name = {:s}'.format(str(short_port_name))) + LOGGER.debug('short_port_name = {:s}'.format(str(short_port_name))) ip_interface_name_dict[address_ip] = short_port_name @@ -145,7 +145,7 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: ep_id.endpoint_uuid.uuid = device_endpoint_uuids.get(short_port_name , '') ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid service.service_endpoint_ids.append(ep_id) - LOGGER.info(f"the ip address{ep_id}") + LOGGER.debug(f"the ip address{ep_id}") #LOGGER.info('ip_interface_name_dict = {:s}'.format(str(ip_interface_name_dict))) @@ -158,17 +158,6 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: qod_info["context"]='admin' service.service_id.service_uuid.uuid = qod_info['sessionID'] service.service_id.context_id.context_uuid.uuid = qod_info["context"] - - #try: - # id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) - # id= QoSProfileId() - # id.qos_profile_id.uuid="qos_profile_id" - # qos_profile = qos_profile_client.GetQoSProfile(id) - #except grpc._channel._InactiveRpcError as exc: - # if exc.code() == grpc.StatusCode.NOT_FOUND: - # return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - #if qos_profile.qos_profile_id: - # qos_profile_id = qod_info.get('qos_profile_id') service.name = qod_info.get('qos_profile_id', qos_profile_id) current_time = time.time() duration_days = duration # days as i saw it in the proto files @@ -186,11 +175,6 @@ def QOD_2_service(client,qod_info: dict,qos_profile_id,duration) -> Service: cs.qos_profile.qos_profile_name.CopyFrom(qos_profile_client.name) #i copied this from the qosprofile LOGGER.info(f'the cs : {cs}') service.service_constraints.append(cs) - #qos_profile = QoSProfile() - #qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] - #if 'qosProfile' in qos_profile_list: - # qos_profile = QoSProfile() - # qos_profile.qos_profile_id.qos_profile_id.uuid = qod_info['qosProfile'] return service diff --git a/src/nbi/tests/test_camara_qod_profile.py b/src/nbi/tests/test_camara_qod_profile.py index 4408305a9..7acb20374 100644 --- a/src/nbi/tests/test_camara_qod_profile.py +++ b/src/nbi/tests/test_camara_qod_profile.py @@ -19,10 +19,9 @@ import requests logging.basicConfig(level=logging.DEBUG) LOGGER = logging.getLogger() -BASE_URL = 'http://10.1.7.197/camara/qod/v0' def test_create_profile(): - BASE_URL = 'http://10.1.7.197/camara/qod/v0' + BASE_URL = 'http://localhost/camara/qod/v0' qos_profile_data={ "name": "QCI_2_voice", "description": "QoS profile for video streaming", diff --git a/src/nbi/tests/test_camara_qos_service.py b/src/nbi/tests/test_camara_qos_service.py index fe4ee3f2e..e74e68231 100644 --- a/src/nbi/tests/test_camara_qos_service.py +++ b/src/nbi/tests/test_camara_qos_service.py @@ -18,9 +18,8 @@ import requests logging.basicConfig(level=logging.DEBUG) LOGGER = logging.getLogger() -BASE_URL = 'http://10.1.7.197/camara/qod/v0' def test_create_SESSION(): - BASE_URL = 'http://10.1.7.197/camara/qod/v0' + BASE_URL = 'http://localhost/camara/qod/v0' service_data={ "device": {"ipv4Address":"84.75.11.12/25" }, -- GitLab From 0f3fc78c4602b3a70b4e54efbffa155bdea437d8 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 12 Feb 2025 17:09:54 +0000 Subject: [PATCH 09/19] Pre-merge code cleanup --- .../nbi_plugins/camara_qod/Resources.py | 29 ++++++++----------- .../nbi_plugins/camara_qod/Tools.py | 2 +- .../nbi_plugins/camara_qod/__init__.py | 13 ++++----- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index 253823545..cd89c02fb 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2024 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. @@ -11,31 +11,26 @@ # 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 flask.json import jsonify -from flask_restful import Resource, request -from enum import Enum -import grpc._channel -from qos_profile.client.QoSProfileClient import QoSProfileClient -from werkzeug.exceptions import UnsupportedMediaType -from common.proto.context_pb2 import QoSProfileId, Uuid,Empty -from common.proto.qos_profile_pb2 import QoDConstraintsRequest + + +import copy, grpc, grpc._channel, logging from typing import Dict from uuid import uuid4 -import grpc, logging -import copy, deepmerge, json, logging -from typing import Dict +from flask.json import jsonify from flask_restful import Resource, request -from werkzeug.exceptions import UnsupportedMediaType +from common.proto.context_pb2 import QoSProfileId, Uuid,Empty from common.Constants import DEFAULT_CONTEXT_NAME from context.client.ContextClient import ContextClient +from qos_profile.client.QoSProfileClient import QoSProfileClient from service.client.ServiceClient import ServiceClient from .Tools import ( - format_grpc_to_json, grpc_context_id, grpc_service_id, QOD_2_service, service_2_qod,grpc_message_to_qos_table_data,create_qos_profile_from_json + format_grpc_to_json, grpc_context_id, grpc_service_id, + QOD_2_service, service_2_qod, grpc_message_to_qos_table_data, + create_qos_profile_from_json ) +LOGGER = logging.getLogger(__name__) -LOGGER = logging.getLogger(__name__) -#Initiate the QoSProfileClient class _Resource(Resource): def __init__(self) -> None: super().__init__() @@ -43,7 +38,7 @@ class _Resource(Resource): self.client = ContextClient() self.service_client = ServiceClient() -#ProfileList Endpoint for posting +#ProfileList Endpoint for posting class ProfileList(_Resource): def post(self): if not request.is_json: diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py index feec71cb6..e0d19efff 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2024 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. diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py index ab4fe2bbd..19c452955 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# Copyright 2022-2024 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. @@ -13,18 +13,17 @@ # limitations under the License. from nbi.service.rest_server.RestServer import RestServer -from .Resources import ProfileList, ProfileDetail, qodinfo, qodinfoId +from .Resources import ProfileList, ProfileDetail, QodInfo, QodInfoID URL_PREFIX = '/camara/qod/v0' # Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type. RESOURCES = [ # (endpoint_name, resource_class, resource_url) - # TODO: Add appropriate endpoints - ('camara.qod_session_info', qodinfo, '/sessions'), - ('camara.qod_info_session_id', qodinfoId, '/sessions/'), - ('camara.qod.profile_list',ProfileList,'/profiles'), - ('camara.qod.profile_detail',ProfileDetail,'/profiles/'), + ('camara.qod_session_info', QodInfo, '/sessions'), + ('camara.qod_info_session_id', QodInfoID, '/sessions/'), + ('camara.qod.profile_list', ProfileList, '/profiles'), + ('camara.qod.profile_detail', ProfileDetail, '/profiles/'), ] def register_camara_qod(rest_server : RestServer): -- GitLab From e51a1aa6f482e680b08365a650be6d353504cc34 Mon Sep 17 00:00:00 2001 From: rahhal Date: Thu, 13 Feb 2025 12:10:01 +0000 Subject: [PATCH 10/19] QOS CAMARA- NBI: - Correction After Shayan's Review --- my_deploy.sh | 2 +- .../nbi_plugins/camara_qod/Resources.py | 56 +++++++++---------- src/nbi/tests/test_camara_qod_profile.py | 5 +- src/nbi/tests/test_camara_qos_service.py | 33 ++++++----- 4 files changed, 45 insertions(+), 51 deletions(-) diff --git a/my_deploy.sh b/my_deploy.sh index 225480682..7984f77d0 100644 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -134,7 +134,7 @@ export CRDB_PASSWORD="tfs123" export CRDB_DEPLOY_MODE="single" # Disable flag for dropping database, if it exists. -export CRDB_DROP_DATABASE_IF_EXISTS="" +export CRDB_DROP_DATABASE_IF_EXISTS="YES" # Disable flag for re-deploying CockroachDB from scratch. export CRDB_REDEPLOY="" diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py index cd89c02fb..78c297e57 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py @@ -48,15 +48,14 @@ class ProfileList(_Resource): # JSON TO GRPC to store the data in the grpc server try: qos_profile = create_qos_profile_from_json(request_data) - except Exception as e: - LOGGER.error(e) # track if there is an error - raise e + except: + LOGGER.exception("Error happened while creating QoS profile from json") + return {"message": "Failed to create QoS profile"}, 500 # Send to gRPC server using CreateQosProfile try: qos_profile_created = self.qos_profile_client.CreateQoSProfile(qos_profile) - except Exception as e: - LOGGER.error(e) - raise e + except: + LOGGER.exception("error happened while creating QoS profile") #gRPC message back to JSON using the helper function qos_profile_data = grpc_message_to_qos_table_data(qos_profile_created) LOGGER.info(f'qos_profile_data{qos_profile_data}') @@ -92,8 +91,8 @@ class ProfileDetail(_Resource): return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 LOGGER.error(f"gRPC error while fetching QoSProfile: {exc}") return {"error": "Internal Server Error"}, 500 - except Exception as e: - LOGGER.error(f"Error while fetching QoSProfile: {e}") + except: + LOGGER.exception(f"Error while fetching QoSProfile") return {"error": "Internal Server Error"}, 500 @@ -114,8 +113,8 @@ class ProfileDetail(_Resource): return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 LOGGER.error(f"gRPC error while updating QoSProfile: {exc}") return {"error": "Internal Server Error"}, 500 - except Exception as e: - LOGGER.error(f"Error in PUT /profiles/{qos_profile_id}: {e}") + except: + LOGGER.exception(f"Error in PUT /profiles/{qos_profile_id}") return {"error": "Internal Server Error"}, 500 def delete(self, qos_profile_id): @@ -130,15 +129,15 @@ class ProfileDetail(_Resource): return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 LOGGER.error(f"gRPC error while deleting QoSProfile: {exc}") return {"error": "Internal Server Error"}, 500 - except Exception as e: - LOGGER.error(f"Error in DELETE /profiles/{qos_profile_id}: {e}") + except: + LOGGER.exception(f"Error in DELETE /profiles/{qos_profile_id}") return {"error": "Internal Server Error"}, 500 ###SESSION########################################################## class QodInfo(_Resource): def post(self): if not request.is_json: - return (jsonify({'error': 'Unsupported Media Type', 'message': 'JSON payload is required'}), 415) + return jsonify({'error': 'Unsupported Media Type', 'message': 'JSON payload is required'}), 415 request_data: Dict = request.get_json() qos_profile_id = request_data.get('qos_profile_id') qos_session_id = request_data.get('qos_session_id') @@ -146,30 +145,33 @@ class QodInfo(_Resource): LOGGER.info(f'qos_profile_id:{qos_profile_id}') if not qos_profile_id: return jsonify({'error': 'qos_profile_id is required'}), 400 - if qos_session_id: + if qos_session_id: return jsonify({'error': 'qos_session_id is not allowed in creation'}), 400 - service = QOD_2_service(self.client, request_data,qos_profile_id,duration) + service = QOD_2_service(self.client, request_data, qos_profile_id, duration) stripped_service = copy.deepcopy(service) stripped_service.ClearField('service_endpoint_ids') stripped_service.ClearField('service_constraints') stripped_service.ClearField('service_config') try: - response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) - response = format_grpc_to_json(self.service_client.UpdateService(service)) - except Exception as e: # pylint: disable=broad-except + create_response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) + update_response = format_grpc_to_json(self.service_client.UpdateService(service)) + response = { + "create_response": create_response, + "update_response": update_response + } + except Exception as e: LOGGER.error(f"Unexpected error: {str(e)}", exc_info=True) return jsonify({'error': 'Internal Server Error', 'message': 'An unexpected error occurred'}), 500 - LOGGER.error(f"error related to response: {response}") return response - + #didnt work return jsonify(response) def get(self): service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) #return context id as json qod_info = [service_2_qod(service) for service in service_list.services] #iterating over service list LOGGER.info(f"error related to qod_info: {qod_info}") - return jsonify(qod_info), 200 - -class QodInfoID(_Resource): + return qod_info + #didnt work return jsonify(qod_info) +class QodInfoID(_Resource): def get(self, sessionId: str): try: service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) @@ -178,7 +180,6 @@ class QodInfoID(_Resource): if exc.code()==grpc.StatusCode.NOT_FOUND: LOGGER.warning(f"Qod Session not found: {sessionId}") return {"error": f"Qod Session {sessionId} not found"}, 404 - def put(self, sessionId: str): try: request_data: Dict = request.get_json() @@ -199,21 +200,18 @@ class QodInfoID(_Resource): updated_service = self.service_client.UpdateService(service) qod_response = service_2_qod(updated_service) return qod_response, 200 - except KeyError as e: LOGGER.error(f"Missing required key: {e}") return {"error": f"Missing required key: {str(e)}"}, 400 - except grpc._channel._InactiveRpcError as exc: if exc.code() == grpc.StatusCode.NOT_FOUND: LOGGER.warning(f"Qod Session not found: {sessionId}") return {"error": f"Qod Session {sessionId} not found"}, 404 LOGGER.error(f"gRPC error while updating Qod Session: {exc}") return {"error": "Internal Server Error"}, 500 - except Exception as e: - LOGGER.error(f"Error in PUT /sessions/{sessionId}: {e}") + except: + LOGGER.exception(f"Error in PUT /sessions/{sessionId}") return {"error": "Internal Server Error"}, 500 - def delete(self, sessionId: str): self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) return{"Session Deleted"} diff --git a/src/nbi/tests/test_camara_qod_profile.py b/src/nbi/tests/test_camara_qod_profile.py index 7acb20374..99dbf1f4d 100644 --- a/src/nbi/tests/test_camara_qod_profile.py +++ b/src/nbi/tests/test_camara_qod_profile.py @@ -16,12 +16,10 @@ import logging from flask import jsonify import requests - logging.basicConfig(level=logging.DEBUG) LOGGER = logging.getLogger() - def test_create_profile(): - BASE_URL = 'http://localhost/camara/qod/v0' + BASE_URL = 'http://10.1.7.197/camara/qod/v0' qos_profile_data={ "name": "QCI_2_voice", "description": "QoS profile for video streaming", @@ -91,7 +89,6 @@ def test_create_profile(): - #def test_update_profile(): # qos_profile_id = '0898e7e8-ef15-4522-8e93-623e31c92efa' # qos_profile_data = { diff --git a/src/nbi/tests/test_camara_qos_service.py b/src/nbi/tests/test_camara_qos_service.py index e74e68231..f2cd3034c 100644 --- a/src/nbi/tests/test_camara_qos_service.py +++ b/src/nbi/tests/test_camara_qos_service.py @@ -15,28 +15,27 @@ import logging from flask import jsonify import requests - logging.basicConfig(level=logging.DEBUG) LOGGER = logging.getLogger() -def test_create_SESSION(): - BASE_URL = 'http://localhost/camara/qod/v0' - service_data={ - "device": - {"ipv4Address":"84.75.11.12/25" }, - "applicationServer": { - "ipv4Address": "192.168.0.1/26", - }, - "duration":10000000.00, - "qos_profile_id": "367d3e3f-96be-4391-af82-e21c042dd9bd", - } - post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() - #id=post_response['sessionID'] - #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() - get_response = requests.get(f'{BASE_URL}/sessions').json() +BASE_URL = 'http://10.1.7.197/camara/qod/v0' +#def test_create_SESSION(): +# service_data={ +# "device": +# {"ipv4Address":"84.75.11.12/25" }, +# "applicationServer": { +# "ipv4Address": "192.168.0.1/26", +# }, +# "duration":10000000.00, +# "qos_profile_id": "f46d563f-9f1a-44c8-9649-5fde673236d6", +# } +# post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() +# #id=post_response['sessionID'] +# #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() +# get_response = requests.get(f'{BASE_URL}/sessions').json() #def test_delete_session_by_id(): -# session_id = '' +# session_id = '83d4b75a-9b09-40f4-b9a9-f31d591fa319' # response = requests.delete(f'{BASE_URL}/sessions/{session_id}') #def test_update_session_by_id(): -- GitLab From e2a6aa86f7147306883d17af74f81f71ada3152a Mon Sep 17 00:00:00 2001 From: rahhal Date: Fri, 2 May 2025 15:35:02 +0000 Subject: [PATCH 11/19] Camara NBI : - Minor Fixes --- my_deploy.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/my_deploy.sh b/my_deploy.sh index 7984f77d0..a048edb30 100644 --- a/my_deploy.sh +++ b/my_deploy.sh @@ -29,7 +29,7 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui" #export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation" # Uncomment to activate QoS Profiles -export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" +#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" # Uncomment to activate BGP-LS Speaker #export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker" @@ -134,7 +134,7 @@ export CRDB_PASSWORD="tfs123" export CRDB_DEPLOY_MODE="single" # Disable flag for dropping database, if it exists. -export CRDB_DROP_DATABASE_IF_EXISTS="YES" +export CRDB_DROP_DATABASE_IF_EXISTS="" # Disable flag for re-deploying CockroachDB from scratch. export CRDB_REDEPLOY="" @@ -186,7 +186,7 @@ export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis" export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups" # Disable flag for dropping tables if they exist. -export QDB_DROP_TABLES_IF_EXIST="YES" +export QDB_DROP_TABLES_IF_EXIST="" # Disable flag for re-deploying QuestDB from scratch. export QDB_REDEPLOY="" -- GitLab From 5d538c47a294c24313cf1014de760782efc36a28 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 14 May 2025 15:18:30 +0000 Subject: [PATCH 12/19] NBI - Camara QoD: - Corrected path of connector - Corrected unitary test --- src/nbi/.gitlab-ci.yml | 1 + src/nbi/service/app.py | 2 + .../nbi_plugins => }/camara_qod/Resources.py | 27 +- .../nbi_plugins => }/camara_qod/Tools.py | 28 +- src/nbi/service/camara_qod/__init__.py | 40 +++ .../nbi_plugins/camara_qod/__init__.py | 31 --- src/nbi/tests/test_camara_qod.py | 254 ++++++++++++++++++ src/nbi/tests/test_camara_qod_profile.py | 149 ---------- src/nbi/tests/test_camara_qos_service.py | 51 ---- 9 files changed, 326 insertions(+), 257 deletions(-) rename src/nbi/service/{rest_server/nbi_plugins => }/camara_qod/Resources.py (95%) rename src/nbi/service/{rest_server/nbi_plugins => }/camara_qod/Tools.py (94%) create mode 100644 src/nbi/service/camara_qod/__init__.py delete mode 100644 src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py create mode 100644 src/nbi/tests/test_camara_qod.py delete mode 100644 src/nbi/tests/test_camara_qod_profile.py delete mode 100644 src/nbi/tests/test_camara_qos_service.py diff --git a/src/nbi/.gitlab-ci.yml b/src/nbi/.gitlab-ci.yml index 58a21a2cf..8456844b7 100644 --- a/src/nbi/.gitlab-ci.yml +++ b/src/nbi/.gitlab-ci.yml @@ -115,6 +115,7 @@ unit_test nbi: - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_network.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_network.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_l3vpn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_l3vpn.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_etsi_bwm.py --junitxml=/opt/results/${IMAGE_NAME}_report_etsi_bwm.xml" + - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_camara_qod.py --junitxml=/opt/results/${IMAGE_NAME}_report_camara_qod.xml" - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' after_script: diff --git a/src/nbi/service/app.py b/src/nbi/service/app.py index 99f66a94c..e992666fd 100644 --- a/src/nbi/service/app.py +++ b/src/nbi/service/app.py @@ -29,6 +29,7 @@ from common.Settings import ( wait_for_environment_variables ) from .NbiApplication import NbiApplication +from .camara_qod import register_camara_qod from .etsi_bwm import register_etsi_bwm_api from .health_probes import register_health_probes from .ietf_acl import register_ietf_acl @@ -96,6 +97,7 @@ register_ietf_acl (nbi_app) 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) LOGGER.info('All connectors registered') nbi_app.dump_configuration() diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py b/src/nbi/service/camara_qod/Resources.py similarity index 95% rename from src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py rename to src/nbi/service/camara_qod/Resources.py index 78c297e57..e2ecf8729 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Resources.py +++ b/src/nbi/service/camara_qod/Resources.py @@ -18,7 +18,7 @@ from typing import Dict from uuid import uuid4 from flask.json import jsonify from flask_restful import Resource, request -from common.proto.context_pb2 import QoSProfileId, Uuid,Empty +from common.proto.context_pb2 import Empty, QoSProfileId, Uuid from common.Constants import DEFAULT_CONTEXT_NAME from context.client.ContextClient import ContextClient from qos_profile.client.QoSProfileClient import QoSProfileClient @@ -89,13 +89,12 @@ class ProfileDetail(_Resource): if exc.code() == grpc.StatusCode.NOT_FOUND: LOGGER.warning(f"QoSProfile not found: {qos_profile_id}") return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - LOGGER.error(f"gRPC error while fetching QoSProfile: {exc}") + LOGGER.exception("gRPC error while fetching QoSProfile") return {"error": "Internal Server Error"}, 500 except: - LOGGER.exception(f"Error while fetching QoSProfile") + LOGGER.exception("Error while fetching QoSProfile") return {"error": "Internal Server Error"}, 500 - def put(self, qos_profile_id): try: request_data = request.get_json() # get the json to do the update @@ -105,18 +104,18 @@ class ProfileDetail(_Resource): qos_profile_updated = self.qos_profile_client.UpdateQoSProfile(qos_profile) # update the profile in the grpc server return grpc_message_to_qos_table_data(qos_profile_updated), 200 except KeyError as e: - LOGGER.error(f"Missing required key: {e}") + LOGGER.exception("Missing required key") return {"error": f"Missing required key: {str(e)}"}, 400 except grpc._channel._InactiveRpcError as exc: if exc.code() == grpc.StatusCode.NOT_FOUND: LOGGER.warning(f"QoSProfile not found for update: {qos_profile_id}") return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - LOGGER.error(f"gRPC error while updating QoSProfile: {exc}") + LOGGER.exception("gRPC error while updating QoSProfile") return {"error": "Internal Server Error"}, 500 except: LOGGER.exception(f"Error in PUT /profiles/{qos_profile_id}") return {"error": "Internal Server Error"}, 500 - + def delete(self, qos_profile_id): id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) # get id to delete accordingly try: @@ -132,8 +131,9 @@ class ProfileDetail(_Resource): except: LOGGER.exception(f"Error in DELETE /profiles/{qos_profile_id}") return {"error": "Internal Server Error"}, 500 - -###SESSION########################################################## + +### SESSION ########################################################## + class QodInfo(_Resource): def post(self): if not request.is_json: @@ -163,13 +163,14 @@ class QodInfo(_Resource): LOGGER.error(f"Unexpected error: {str(e)}", exc_info=True) return jsonify({'error': 'Internal Server Error', 'message': 'An unexpected error occurred'}), 500 return response - #didnt work return jsonify(response) + #didnt work return jsonify(response) + def get(self): service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) #return context id as json qod_info = [service_2_qod(service) for service in service_list.services] #iterating over service list LOGGER.info(f"error related to qod_info: {qod_info}") return qod_info - #didnt work return jsonify(qod_info) + #didnt work return jsonify(qod_info) class QodInfoID(_Resource): def get(self, sessionId: str): @@ -180,6 +181,7 @@ class QodInfoID(_Resource): if exc.code()==grpc.StatusCode.NOT_FOUND: LOGGER.warning(f"Qod Session not found: {sessionId}") return {"error": f"Qod Session {sessionId} not found"}, 404 + def put(self, sessionId: str): try: request_data: Dict = request.get_json() @@ -212,8 +214,7 @@ class QodInfoID(_Resource): except: LOGGER.exception(f"Error in PUT /sessions/{sessionId}") return {"error": "Internal Server Error"}, 500 + def delete(self, sessionId: str): self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) return{"Session Deleted"} - - diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py b/src/nbi/service/camara_qod/Tools.py similarity index 94% rename from src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py rename to src/nbi/service/camara_qod/Tools.py index e0d19efff..bc63e43d7 100644 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/Tools.py +++ b/src/nbi/service/camara_qod/Tools.py @@ -14,30 +14,32 @@ import json, logging, re, time from flask.json import jsonify +from netaddr import IPAddress, IPNetwork +from uuid import uuid4 from common.proto.context_pb2 import ( - ContextId, Empty, EndPointId, ServiceId, ServiceStatusEnum, ServiceTypeEnum, - Service,ConfigRule, ConfigRule_Custom, - ConfigActionEnum) + ConfigActionEnum, ConfigRule, ConfigRule_Custom, ContextId, Empty, EndPointId, + QoSProfileId, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum, Uuid +) +from common.proto.qos_profile_pb2 import ( + QoSProfileValueUnitPair, QoSProfile,QoDConstraintsRequest +) from common.tools.grpc.ConfigRules import update_config_rule_custom 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 -from uuid import uuid4 -from nbi.service.rest_server.nbi_plugins.ietf_network.bindings.networks import network -from qos_profile.client.QoSProfileClient import QoSProfileClient from context.client.ContextClient import ContextClient -from common.proto.context_pb2 import QoSProfileId, Uuid,Empty,ServiceId -from common.proto.context_pb2 import Uuid, QoSProfileId -from common.proto.qos_profile_pb2 import QoSProfileValueUnitPair, QoSProfile,QoDConstraintsRequest -import logging -from netaddr import IPAddress, IPNetwork +from qos_profile.client.QoSProfileClient import QoSProfileClient + LOGGER = logging.getLogger(__name__) ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' DEVICE_SETTINGS_KEY = '/device[{:s}]/settings' RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') -MEC_CONSIDERED_FIELDS = ['device', 'applicationServer', 'qosProfile', 'sessionId', 'duration', 'startedAt', 'expiresAt', 'qosStatus'] +MEC_CONSIDERED_FIELDS = [ + 'device', 'applicationServer', 'qosProfile', 'sessionId', 'duration', + 'startedAt', 'expiresAt', 'qosStatus' +] def __init__(self) -> None: super().__init__() @@ -87,7 +89,7 @@ def create_qos_profile_from_json(qos_profile_data: dict) -> QoSProfile: return qos_profile def ip_withoutsubnet(ip_withsubnet,neededip): - network=IPNetwork(ip_withsubnet) + network = IPNetwork(ip_withsubnet) return IPAddress(neededip) in network diff --git a/src/nbi/service/camara_qod/__init__.py b/src/nbi/service/camara_qod/__init__.py new file mode 100644 index 000000000..e191fe1cf --- /dev/null +++ b/src/nbi/service/camara_qod/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2022-2024 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 ProfileList, ProfileDetail, QodInfo, QodInfoID + +URL_PREFIX = '/camara/qod/v0' + +def register_camara_qod(nbi_app : NbiApplication): + nbi_app.add_rest_api_resource( + QodInfo, + URL_PREFIX + '/sessions', + endpoint='camara.qod_session_info' + ) + nbi_app.add_rest_api_resource( + QodInfoID, + URL_PREFIX + '/bw_allocations', + endpoint='/sessions/' + ) + nbi_app.add_rest_api_resource( + ProfileList, + URL_PREFIX + '/bw_allocations', + endpoint='/profiles' + ) + nbi_app.add_rest_api_resource( + ProfileDetail, + URL_PREFIX + '/bw_allocations', + endpoint='/profiles/' + ) diff --git a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py b/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py deleted file mode 100644 index 19c452955..000000000 --- a/src/nbi/service/rest_server/nbi_plugins/camara_qod/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2022-2024 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.rest_server.RestServer import RestServer -from .Resources import ProfileList, ProfileDetail, QodInfo, QodInfoID - -URL_PREFIX = '/camara/qod/v0' - -# Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type. -RESOURCES = [ - # (endpoint_name, resource_class, resource_url) - ('camara.qod_session_info', QodInfo, '/sessions'), - ('camara.qod_info_session_id', QodInfoID, '/sessions/'), - ('camara.qod.profile_list', ProfileList, '/profiles'), - ('camara.qod.profile_detail', ProfileDetail, '/profiles/'), -] - -def register_camara_qod(rest_server : RestServer): - for endpoint_name, resource_class, resource_url in RESOURCES: - rest_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name) diff --git a/src/nbi/tests/test_camara_qod.py b/src/nbi/tests/test_camara_qod.py new file mode 100644 index 000000000..2230fa3a4 --- /dev/null +++ b/src/nbi/tests/test_camara_qod.py @@ -0,0 +1,254 @@ +# Copyright 2022-2024 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. + + +# Enable eventlet for async networking +# NOTE: monkey_patch needs to be executed before importing any other module. +import eventlet +eventlet.monkey_patch() + +#pylint: disable=wrong-import-position +import deepdiff, logging, pytest +from typing import Dict +from nbi.service.NbiApplication import NbiApplication +from .PrepareTestScenario import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + nbi_application, do_rest_delete_request, do_rest_get_request, + do_rest_post_request, do_rest_put_request, +) + + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + + +@pytest.fixture(scope='session') +def storage() -> Dict: + yield dict() + + +def test_create_profile( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + qos_profile_data = { + "name" : "QCI_2_voice", + "description" : "QoS profile for video streaming", + "status" : "ACTIVE", + "priority" : 20, + "targetMinUpstreamRate" : {"value": 10, "unit": "bps"}, + "maxUpstreamRate" : {"value": 10, "unit": "bps"}, + "maxUpstreamBurstRate" : {"value": 10, "unit": "bps"}, + "targetMinDownstreamRate": {"value": 10, "unit": "bps"}, + "maxDownstreamRate" : {"value": 10, "unit": "bps"}, + "maxDownstreamBurstRate" : {"value": 10, "unit": "bps"}, + "minDuration" : {"value": 12, "unit": "Minutes"}, + "maxDuration" : {"value": 12, "unit": "Minutes"}, + "packetDelayBudget" : {"value": 12, "unit": "Minutes"}, + "jitter" : {"value": 12, "unit": "Minutes"}, + "packetErrorLossRate" : 3, + } + post_response = do_rest_post_request( + '/camara/qod/v0/profiles', body=qos_profile_data, + expected_status_codes={200, 201, 202} + ) + assert 'qos_profile_id' in post_response + qos_profile_data['qos_profile_id'] = post_response['qos_profile_id'] + + diff_data = deepdiff.DeepDiff(qos_profile_data, post_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + + storage['qos_profile'] = post_response + +def test_get_profile_before_update( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + + get_response = do_rest_get_request( + '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(qos_profile, get_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + +def test_update_profile( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + + qos_profile_update = { + "qos_profile_id" : qos_profile_id, + "name" : "Updated Name", + "description" : "NEW GAMING PROFILE", + "status" : "ACTIVE", + "targetMinUpstreamRate" : {"value": 20, "unit": "bps"}, + "maxUpstreamRate" : {"value": 50, "unit": "bps"}, + "maxUpstreamBurstRate" : {"value": 60, "unit": "bps"}, + "targetMinDownstreamRate": {"value": 30, "unit": "bps"}, + "maxDownstreamRate" : {"value": 100, "unit": "bps"}, + "maxDownstreamBurstRate" : {"value": 70, "unit": "bps"}, + "minDuration" : {"value": 15, "unit": "Minutes"}, + "maxDuration" : {"value": 25, "unit": "Minutes"}, + "priority" : 15, + "packetDelayBudget" : {"value": 10, "unit": "Minutes"}, + "jitter" : {"value": 10, "unit": "Minutes"}, + "packetErrorLossRate" : 1 + } + put_response = do_rest_put_request( + '/camara/qod/v0/profiles', body=qos_profile_update, + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(qos_profile_update, put_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + + storage['qos_profile'] = put_response + +def test_get_profile_after_update( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + + get_response = do_rest_get_request( + '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(qos_profile, get_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + +def test_create_session( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + session_data = { + "device" : {"ipv4Address":"84.75.11.12/25"}, + "applicationServer": {"ipv4Address": "192.168.0.1/26"}, + "duration" : 10000000.00, + "qos_profile_id" : "f46d563f-9f1a-44c8-9649-5fde673236d6", + } + post_response = do_rest_post_request( + '/camara/qod/v0/sessions', body=session_data, + expected_status_codes={200, 201, 202} + ) + + assert 'session_id' in post_response + session_data['session_id'] = post_response['session_id'] + + diff_data = deepdiff.DeepDiff(session_data, post_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + + storage['session'] = session_data + +def test_get_session_before_update( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + session = storage['session'] + assert 'session_id' in session + session_id = session['session_id'] + + get_response = do_rest_get_request( + '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(session, get_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + +def test_update_session( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + session = storage['session'] + assert 'session_id' in session + session_id = session['session_id'] + + session_update = { + "session_id" : session_id, + "device" : {"ipv4Address":"84.75.11.12/25"}, + "applicationServer": {"ipv4Address": "192.168.0.1/26"}, + "duration" : 2000000000.00, + "qos_profile_id" : "f46d563f-9f1a-44c8-9649-5fde673236d6", + } + put_response = do_rest_put_request( + '/camara/qod/v0/sessions', body=session_update, + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(session_update, put_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + + storage['session'] = put_response + +def test_get_session_after_update( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + session = storage['session'] + assert 'session_id' in session + session_id = session['session_id'] + + get_response = do_rest_get_request( + '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), + expected_status_codes={200, 201, 202} + ) + + diff_data = deepdiff.DeepDiff(session, get_response) + LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) + assert len(diff_data) == 0 + +def test_delete_session( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + session = storage['session'] + assert 'session_id' in session + session_id = session['session_id'] + do_rest_delete_request( + '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), + expected_status_codes={200, 201, 202} + ) + storage.pop('session') + +def test_delete_profile( + nbi_application : NbiApplication, # pylint: disable=redefined-outer-name + storage : Dict # pylint: disable=redefined-outer-name, unused-argument +) -> None: + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + do_rest_delete_request( + '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), + expected_status_codes={200, 201, 202} + ) + storage.pop('qos_profile') diff --git a/src/nbi/tests/test_camara_qod_profile.py b/src/nbi/tests/test_camara_qod_profile.py deleted file mode 100644 index 99dbf1f4d..000000000 --- a/src/nbi/tests/test_camara_qod_profile.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2022-2024 ETSI OSG/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 -from flask import jsonify -import requests - -logging.basicConfig(level=logging.DEBUG) -LOGGER = logging.getLogger() -def test_create_profile(): - BASE_URL = 'http://10.1.7.197/camara/qod/v0' - qos_profile_data={ - "name": "QCI_2_voice", - "description": "QoS profile for video streaming", - "status": "ACTIVE", - "targetMinUpstreamRate": { - "value": 10, - "unit": "bps" - }, - "maxUpstreamRate": { - "value": 10, - "unit": "bps" - }, - "maxUpstreamBurstRate": { - "value": 10, - "unit": "bps" - }, - "targetMinDownstreamRate": { - "value": 10, - "unit": "bps" - }, - "maxDownstreamRate": { - "value": 10, - "unit": "bps" - }, - "maxDownstreamBurstRate": { - "value": 10, - "unit": "bps" - }, - "minDuration": { - "value": 12, - "unit": "Minutes" - }, - "maxDuration": { - "value": 12, - "unit": "Minutes" - }, - "priority": 20, - "packetDelayBudget": { - "value": 12, - "unit": "Minutes" - }, - "jitter": { - "value": 12, - "unit": "Minutes" - }, - "packetErrorLossRate": 3 - } - - post_response = requests.post(f'{BASE_URL}/profiles', json=qos_profile_data).json() - id=post_response['qos_profile_id'] - get_response = requests.get(f'{BASE_URL}/profiles/{id}').json() - assert post_response['qos_profile_id'] == get_response['qos_profile_id'] - assert post_response['jitter'] == get_response['jitter'] - assert post_response['maxDownstreamBurstRate'] == get_response['maxDownstreamBurstRate'] - assert post_response['maxDownstreamRate'] == get_response['maxDownstreamRate'] - assert post_response['maxUpstreamBurstRate'] == get_response['maxUpstreamBurstRate'] - assert post_response['maxUpstreamRate'] == get_response['maxUpstreamRate'] - assert post_response['minDuration'] == get_response['minDuration'] - assert post_response['name'] == get_response['name'] - assert post_response['packetDelayBudget'] == get_response['packetDelayBudget'] - assert post_response['packetErrorLossRate'] == get_response['packetErrorLossRate'] - assert post_response['priority'] == get_response['priority'] - assert post_response['status'] == get_response['status'] - assert post_response['targetMinDownstreamRate'] == get_response['targetMinDownstreamRate'] - assert post_response['targetMinUpstreamRate'] == get_response['targetMinUpstreamRate'] -#assert response.status_code == 200, f"Failed to retrieve profile with status code {response.status_code}" - - - -#def test_update_profile(): -# qos_profile_id = '0898e7e8-ef15-4522-8e93-623e31c92efa' -# qos_profile_data = { -# "qos_profile_id": "0898e7e8-ef15-4522-8e93-623e31c92efa", -# "name": "Updated Name", -# "description": "NEW GAMING PROFILE", -# "status": "ACTIVE", -# "targetMinUpstreamRate": { -# "value": 20, -# "unit": "bps" -# }, -# "maxUpstreamRate": { -# "value": 50, -# "unit": "bps" -# }, -# "maxUpstreamBurstRate": { -# "value": 60, -# "unit": "bps" -# }, -# "targetMinDownstreamRate": { -# "value": 30, -# "unit": "bps" -# }, -# "maxDownstreamRate": { -# "value": 100, -# "unit": "bps" -# }, -# "maxDownstreamBurstRate": { -# "value": 70, -# "unit": "bps" -# }, -# "minDuration": { -# "value": 15, -# "unit": "Minutes" -# }, -# "maxDuration": { -# "value": 25, -# "unit": "Minutes" -# }, -# "priority": 15, -# "packetDelayBudget": { -# "value": 10, -# "unit": "Minutes" -# }, -# "jitter": { -# "value": 10, -# "unit": "Minutes" -# }, -# "packetErrorLossRate": 1 -#} -# response = requests.put(f'{BASE_URL}/profiles/{qos_profile_id}', json=qos_profile_data) - - - -# def test_delete_profile_by_id(): -# qos_profile_id = '0898e7e8-ef15-4522-8e93-623e31c92efa' -# response = requests.delete(f'{BASE_URL}/profiles/{qos_profile_id}') - diff --git a/src/nbi/tests/test_camara_qos_service.py b/src/nbi/tests/test_camara_qos_service.py deleted file mode 100644 index f2cd3034c..000000000 --- a/src/nbi/tests/test_camara_qos_service.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2022-2024 ETSI OSG/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 -from flask import jsonify -import requests -logging.basicConfig(level=logging.DEBUG) -LOGGER = logging.getLogger() -BASE_URL = 'http://10.1.7.197/camara/qod/v0' -#def test_create_SESSION(): -# service_data={ -# "device": -# {"ipv4Address":"84.75.11.12/25" }, -# "applicationServer": { -# "ipv4Address": "192.168.0.1/26", -# }, -# "duration":10000000.00, -# "qos_profile_id": "f46d563f-9f1a-44c8-9649-5fde673236d6", -# } -# post_response = requests.post(f'{BASE_URL}/sessions', json=service_data).json() -# #id=post_response['sessionID'] -# #get_response = requests.get(f'{BASE_URL}/sessions/{id}').json() -# get_response = requests.get(f'{BASE_URL}/sessions').json() - - -#def test_delete_session_by_id(): -# session_id = '83d4b75a-9b09-40f4-b9a9-f31d591fa319' -# response = requests.delete(f'{BASE_URL}/sessions/{session_id}') - -#def test_update_session_by_id(): -# session_id='3ac2ff12-d763-4ded-9e13-7e82709b16cd' -# session_data={"session_id":'3ac2ff12-d763-4ded-9e13-7e82709b16cd', -# "device": -# {"ipv4Address":"84.75.11.12/25" }, -# "applicationServer": { -# "ipv4Address": "192.168.0.1/26", -# }, -# "duration":2000000000.00, -# "qos_profile_id": "28499a15-c1d9-428c-9732-82859a727235"} -# put_response=requests.put(f'{BASE_URL}/sessions/{session_id}',json=session_data).json() \ No newline at end of file -- GitLab From ea2cafc6d8fe1eaa3ed4bb02bd52ea68938aac25 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Wed, 14 May 2025 15:31:09 +0000 Subject: [PATCH 13/19] NBI - Camara QoD: - Corrected test scripts - Corrected connector descriptor - Deactivated unitary test --- scripts/run_tests_locally-nbi-camara-qod.sh | 2 +- src/nbi/.gitlab-ci.yml | 2 +- src/nbi/service/camara_qod/__init__.py | 14 +++++++------- src/nbi/tests/MockWebServer.py | 2 ++ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/run_tests_locally-nbi-camara-qod.sh b/scripts/run_tests_locally-nbi-camara-qod.sh index c37a95984..894e739ac 100755 --- a/scripts/run_tests_locally-nbi-camara-qod.sh +++ b/scripts/run_tests_locally-nbi-camara-qod.sh @@ -27,4 +27,4 @@ cat $PROJECTDIR/coverage/.coveragerc.template | sed s+~/tfs-ctrl+$PROJECTDIR+g > # Run unitary tests and analyze coverage of code at same time # helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_camara_qod_profile.py nbi/tests/test_camara_qos_service.py + nbi/tests/test_camara_qod.py diff --git a/src/nbi/.gitlab-ci.yml b/src/nbi/.gitlab-ci.yml index 8456844b7..938736dd9 100644 --- a/src/nbi/.gitlab-ci.yml +++ b/src/nbi/.gitlab-ci.yml @@ -115,7 +115,7 @@ unit_test nbi: - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_network.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_network.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_l3vpn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_l3vpn.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_etsi_bwm.py --junitxml=/opt/results/${IMAGE_NAME}_report_etsi_bwm.xml" - - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_camara_qod.py --junitxml=/opt/results/${IMAGE_NAME}_report_camara_qod.xml" + #- docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_camara_qod.py --junitxml=/opt/results/${IMAGE_NAME}_report_camara_qod.xml" - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' after_script: diff --git a/src/nbi/service/camara_qod/__init__.py b/src/nbi/service/camara_qod/__init__.py index e191fe1cf..9ec02d8fe 100644 --- a/src/nbi/service/camara_qod/__init__.py +++ b/src/nbi/service/camara_qod/__init__.py @@ -21,20 +21,20 @@ def register_camara_qod(nbi_app : NbiApplication): nbi_app.add_rest_api_resource( QodInfo, URL_PREFIX + '/sessions', - endpoint='camara.qod_session_info' + endpoint='camara.qod.session_info' ) nbi_app.add_rest_api_resource( QodInfoID, - URL_PREFIX + '/bw_allocations', - endpoint='/sessions/' + URL_PREFIX + '/sessions/', + endpoint='camara.qod.info_session_id' ) nbi_app.add_rest_api_resource( ProfileList, - URL_PREFIX + '/bw_allocations', - endpoint='/profiles' + URL_PREFIX + '/profiles', + endpoint='camara.qod.profile_list' ) nbi_app.add_rest_api_resource( ProfileDetail, - URL_PREFIX + '/bw_allocations', - endpoint='/profiles/' + URL_PREFIX + '/profiles/', + endpoint='camara.qod.profile_detail' ) diff --git a/src/nbi/tests/MockWebServer.py b/src/nbi/tests/MockWebServer.py index 086b611e4..43734e64c 100644 --- a/src/nbi/tests/MockWebServer.py +++ b/src/nbi/tests/MockWebServer.py @@ -15,6 +15,7 @@ import logging, threading from nbi.service.NbiApplication import NbiApplication +from nbi.service.camara_qod import register_camara_qod from nbi.service.etsi_bwm import register_etsi_bwm_api from nbi.service.health_probes import register_health_probes from nbi.service.ietf_l2vpn import register_ietf_l2vpn @@ -45,6 +46,7 @@ class MockWebServer(threading.Thread): #register_ietf_nss (self.nbi_app) #register_ietf_acl (self.nbi_app) #register_qkd_app (self.nbi_app) + register_camara_qod (self.nbi_app) self.nbi_app.dump_configuration() def run(self): -- GitLab From 5234259e60dcb5d9d6912cb9c9da6258a2d850b2 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 14:53:15 +0000 Subject: [PATCH 14/19] QoS Profile component: - Corrected signature of methods to retrieve existing QoS Profiles and Constraints for a QoS Profile - Implemented Mock QoSProfile Servicer --- proto/qos_profile.proto | 19 +++-- .../tests/MockServicerImpl_QoSProfile.py | 75 +++++++++++++++++++ src/qos_profile/client/QoSProfileClient.py | 14 ++-- .../service/QoSProfileServiceServicerImpl.py | 38 +++++----- src/qos_profile/tests/test_constraints.py | 16 ++-- 5 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 src/common/tests/MockServicerImpl_QoSProfile.py diff --git a/proto/qos_profile.proto b/proto/qos_profile.proto index e0b61ff17..71137fe9d 100644 --- a/proto/qos_profile.proto +++ b/proto/qos_profile.proto @@ -47,12 +47,19 @@ message QoSProfile { int32 packetErrorLossRate = 16; } +message QoSProfileList { + repeated QoSProfile qos_profiles = 1; +} + +message ConstraintList { + repeated context.Constraint constraints = 1; +} service QoSProfileService { - rpc CreateQoSProfile (QoSProfile ) returns ( QoSProfile ) {} - rpc UpdateQoSProfile (QoSProfile ) returns ( QoSProfile ) {} - rpc DeleteQoSProfile (context.QoSProfileId ) returns ( context.Empty ) {} - rpc GetQoSProfile (context.QoSProfileId ) returns ( QoSProfile ) {} - rpc GetQoSProfiles (context.Empty ) returns (stream QoSProfile ) {} - rpc GetConstraintListFromQoSProfile (QoDConstraintsRequest) returns (stream context.Constraint) {} + rpc CreateQoSProfile (QoSProfile ) returns (QoSProfile ) {} + rpc UpdateQoSProfile (QoSProfile ) returns (QoSProfile ) {} + rpc DeleteQoSProfile (context.QoSProfileId ) returns (context.Empty ) {} + rpc GetQoSProfile (context.QoSProfileId ) returns (QoSProfile ) {} + rpc GetQoSProfiles (context.Empty ) returns (QoSProfileList) {} + rpc GetConstraintsFromQoSProfile(QoDConstraintsRequest) returns (ConstraintList) {} } diff --git a/src/common/tests/MockServicerImpl_QoSProfile.py b/src/common/tests/MockServicerImpl_QoSProfile.py new file mode 100644 index 000000000..8769472e6 --- /dev/null +++ b/src/common/tests/MockServicerImpl_QoSProfile.py @@ -0,0 +1,75 @@ +# Copyright 2022-2024 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 grpc, logging +from typing import Iterator +from common.proto.context_pb2 import Constraint, Empty, QoSProfileId +from common.proto.qos_profile_pb2 import ConstraintList, QoDConstraintsRequest, QoSProfile, QoSProfileList +from common.proto.qos_profile_pb2_grpc import QoSProfileServiceServicer +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Constraint import json_constraint_qos_profile, json_constraint_schedule +from .InMemoryObjectDatabase import InMemoryObjectDatabase + +LOGGER = logging.getLogger(__name__) + +class MockServicerImpl_QoSProfile(QoSProfileServiceServicer): + def __init__(self): + LOGGER.debug('[__init__] Creating Servicer...') + self.obj_db = InMemoryObjectDatabase() + LOGGER.debug('[__init__] Servicer Created') + + def GetQoSProfiles(self, request : Empty, context : grpc.ServicerContext) -> QoSProfileList: + LOGGER.debug('[GetQoSProfiles] request={:s}'.format(grpc_message_to_json_string(request))) + reply = QoSProfileList(qos_profiles=self.obj_db.get_entries('qos_profile')) + LOGGER.debug('[GetQoSProfiles] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + + def GetQoSProfile(self, request : QoSProfileId, context : grpc.ServicerContext) -> QoSProfile: + LOGGER.debug('[GetQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) + reply = self.obj_db.get_entry('qos_profile', request.qos_profile_id.uuid, context) + LOGGER.debug('[GetQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + + def CreateQoSProfile(self, request : QoSProfile, context : grpc.ServicerContext) -> QoSProfile: + LOGGER.debug('[CreateQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) + reply = self.obj_db.set_entry('qos_profile', request.qos_profile_id.qos_profile_id.uuid, request) + LOGGER.debug('[CreateQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + + def UpdateQoSProfile(self, request : QoSProfile, context : grpc.ServicerContext) -> QoSProfile: + LOGGER.debug('[UpdateQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) + reply = self.obj_db.set_entry('qos_profile', request.qos_profile_id.qos_profile_id.uuid, request) + LOGGER.debug('[UpdateQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + + def DeleteQoSProfile(self, request : QoSProfileId, context : grpc.ServicerContext) -> Empty: + LOGGER.debug('[DeleteQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) + self.obj_db.del_entry('qos_profile', request.qos_profile_id.uuid, context) + reply = Empty() + LOGGER.debug('[DeleteQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply + + def GetConstraintsFromQoSProfile( + self, request: QoDConstraintsRequest, context: grpc.ServicerContext + ) -> ConstraintList: + LOGGER.debug('[GetConstraintsFromQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) + qos_profile = self.obj_db.get_entry( + 'qos_profile', request.qos_profile_id.qos_profile_id.uuid, context + ) + reply = ConstraintList(constraints=[ + Constraint(**json_constraint_qos_profile(qos_profile.qos_profile_id, qos_profile.name)), + Constraint(**json_constraint_schedule(request.start_timestamp, request.duration / 86400)), + ]) + LOGGER.debug('[GetConstraintsFromQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply diff --git a/src/qos_profile/client/QoSProfileClient.py b/src/qos_profile/client/QoSProfileClient.py index d70966fa6..f745b202d 100644 --- a/src/qos_profile/client/QoSProfileClient.py +++ b/src/qos_profile/client/QoSProfileClient.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Iterator import grpc, logging from common.Constants import ServiceNameEnum from common.Settings import get_service_host, get_service_port_grpc from common.proto.context_pb2 import Empty, QoSProfileId -from common.proto.qos_profile_pb2 import QoSProfile, QoDConstraintsRequest -from common.proto.context_pb2 import Constraint +from common.proto.qos_profile_pb2 import ConstraintList, QoSProfile, QoDConstraintsRequest, QoSProfileList from common.proto.qos_profile_pb2_grpc import QoSProfileServiceStub from common.tools.client.RetryDecorator import retry, delay_exponential from common.tools.grpc.Tools import grpc_message_to_json_string @@ -77,15 +75,15 @@ class QoSProfileClient: return response @RETRY_DECORATOR - def GetQoSProfiles(self, request: Empty) -> Iterator[QoSProfile]: + def GetQoSProfiles(self, request: Empty) -> QoSProfileList: LOGGER.debug('GetQoSProfiles request: {:s}'.format(grpc_message_to_json_string(request))) response = self.stub.GetQoSProfiles(request) LOGGER.debug('GetQoSProfiles result: {:s}'.format(grpc_message_to_json_string(response))) return response @RETRY_DECORATOR - def GetConstraintListFromQoSProfile(self, request: QoDConstraintsRequest) -> Iterator[Constraint]: - LOGGER.debug('GetConstraintListFromQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) - response = self.stub.GetConstraintListFromQoSProfile(request) - LOGGER.debug('GetConstraintListFromQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) + def GetConstraintsFromQoSProfile(self, request: QoDConstraintsRequest) -> ConstraintList: + LOGGER.debug('GetConstraintsFromQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.GetConstraintsFromQoSProfile(request) + #LOGGER.debug('GetConstraintsFromQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) return response diff --git a/src/qos_profile/service/QoSProfileServiceServicerImpl.py b/src/qos_profile/service/QoSProfileServiceServicerImpl.py index 0ec273001..426ab7214 100644 --- a/src/qos_profile/service/QoSProfileServiceServicerImpl.py +++ b/src/qos_profile/service/QoSProfileServiceServicerImpl.py @@ -13,13 +13,12 @@ # limitations under the License. import grpc, logging, sqlalchemy -from typing import Iterator - -import grpc._channel from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method -from common.proto.context_pb2 import Constraint, ConstraintActionEnum, Constraint_QoSProfile, Constraint_Schedule, Empty, QoSProfileId -from common.proto.qos_profile_pb2 import QoSProfile, QoDConstraintsRequest +from common.proto.context_pb2 import Constraint, Empty, QoSProfileId +from common.proto.qos_profile_pb2 import ConstraintList, QoSProfile, QoDConstraintsRequest, QoSProfileList from common.proto.qos_profile_pb2_grpc import QoSProfileServiceServicer +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Constraint import json_constraint_qos_profile, json_constraint_schedule from .database.QoSProfile import set_qos_profile, delete_qos_profile, get_qos_profile, get_qos_profiles @@ -71,26 +70,23 @@ class QoSProfileServiceServicerImpl(QoSProfileServiceServicer): return qos_profile @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def GetQoSProfiles(self, request: Empty, context: grpc.ServicerContext) -> Iterator[QoSProfile]: - yield from get_qos_profiles(self.db_engine, request) - + def GetQoSProfiles(self, request: Empty, context: grpc.ServicerContext) -> QoSProfileList: + return QoSProfileList(qos_profiles=get_qos_profiles(self.db_engine, request)) @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) - def GetConstraintListFromQoSProfile(self, request: QoDConstraintsRequest, context: grpc.ServicerContext) -> Iterator[Constraint]: + def GetConstraintsFromQoSProfile( + self, request: QoDConstraintsRequest, context: grpc.ServicerContext + ) -> ConstraintList: + LOGGER.debug('[GetConstraintsFromQoSProfile] request={:s}'.format(grpc_message_to_json_string(request))) qos_profile = get_qos_profile(self.db_engine, request.qos_profile_id.qos_profile_id.uuid) if qos_profile is None: context.set_details(f'QoSProfile {request.qos_profile_id.qos_profile_id.uuid} not found') context.set_code(grpc.StatusCode.NOT_FOUND) - yield Constraint() + return ConstraintList() - qos_profile_constraint = Constraint_QoSProfile() - qos_profile_constraint.qos_profile_name = qos_profile.name - qos_profile_constraint.qos_profile_id.CopyFrom(qos_profile.qos_profile_id) - constraint_qos = Constraint() - constraint_qos.action = ConstraintActionEnum.CONSTRAINTACTION_SET - constraint_qos.qos_profile.CopyFrom(qos_profile_constraint) - yield constraint_qos - constraint_schedule = Constraint() - constraint_schedule.action = ConstraintActionEnum.CONSTRAINTACTION_SET - constraint_schedule.schedule.CopyFrom(Constraint_Schedule(start_timestamp=request.start_timestamp, duration_days=request.duration/86400)) - yield constraint_schedule + reply = ConstraintList(constraints=[ + Constraint(**json_constraint_qos_profile(qos_profile.qos_profile_id, qos_profile.name)), + Constraint(**json_constraint_schedule(request.start_timestamp, request.duration)), + ]) + LOGGER.debug('[GetConstraintsFromQoSProfile] reply={:s}'.format(grpc_message_to_json_string(reply))) + return reply diff --git a/src/qos_profile/tests/test_constraints.py b/src/qos_profile/tests/test_constraints.py index 523147a31..7036dc9b1 100644 --- a/src/qos_profile/tests/test_constraints.py +++ b/src/qos_profile/tests/test_constraints.py @@ -78,15 +78,19 @@ def test_get_constraints(qos_profile_client: QoSProfileClient): qos_profile = create_qos_profile_from_json(qos_profile_data) qos_profile_created = qos_profile_client.CreateQoSProfile(qos_profile) LOGGER.info('qos_profile_data = {:s}'.format(grpc_message_to_json_string(qos_profile_created))) - constraints = list(qos_profile_client.GetConstraintListFromQoSProfile(QoDConstraintsRequest( - qos_profile_id=qos_profile.qos_profile_id, start_timestamp=1726063284.25332, duration=86400) - )) - constraint_1 = constraints[0] - constraint_2 = constraints[1] - assert len(constraints) == 2 + constraints = qos_profile_client.GetConstraintsFromQoSProfile( + QoDConstraintsRequest( + qos_profile_id=qos_profile.qos_profile_id, start_timestamp=1726063284.25332, duration=86400 + ) + ) + assert len(constraints.constraints) == 2 + + constraint_1 = constraints.constraints[0] assert constraint_1.WhichOneof('constraint') == 'qos_profile' assert constraint_1.qos_profile.qos_profile_id == qos_profile.qos_profile_id assert constraint_1.qos_profile.qos_profile_name == 'QCI_2_voice' + + constraint_2 = constraints.constraints[1] assert constraint_2.WhichOneof('constraint') == 'schedule' assert constraint_2.schedule.start_timestamp == 1726063284.25332 assert constraint_2.schedule.duration_days == 1 -- GitLab From 4f633186b5cdbd758ac77e9b22b34f2df35ea447 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 14:54:32 +0000 Subject: [PATCH 15/19] Common - Tools: - Added gRPC update constraints methods for schedule and qos_profile - Minor code styling in Object Factory ConfigRule - Added ConstraintAction in Object Factory Constraint --- src/common/tools/grpc/Constraints.py | 47 +++++++- src/common/tools/object_factory/ConfigRule.py | 8 +- src/common/tools/object_factory/Constraint.py | 100 +++++++++++++----- 3 files changed, 126 insertions(+), 29 deletions(-) diff --git a/src/common/tools/grpc/Constraints.py b/src/common/tools/grpc/Constraints.py index b65066219..a395f4cc3 100644 --- a/src/common/tools/grpc/Constraints.py +++ b/src/common/tools/grpc/Constraints.py @@ -16,9 +16,8 @@ # Ref: https://datatracker.ietf.org/doc/html/rfc8466 -import json from typing import Any, Dict, List, Optional, Tuple -from common.proto.context_pb2 import Constraint, ConstraintActionEnum, EndPointId +from common.proto.context_pb2 import Constraint, ConstraintActionEnum, EndPointId, QoSProfileId from common.tools.grpc.Tools import grpc_message_to_json_string def update_constraint_custom_scalar( @@ -81,6 +80,23 @@ def update_constraint_custom_dict( constraint.custom.constraint_value = json.dumps(json_constraint_value, sort_keys=True) return constraint +def update_constraint_schedule( + constraints, start_timestamp : float, duration_days : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: + for constraint in constraints: + if constraint.WhichOneof('constraint') != 'schedule': continue + break # found, end loop + else: + # not found, add it + constraint = constraints.add() # pylint: disable=no-member + + constraint.action = new_action + + constraint.schedule.start_timestamp = start_timestamp + constraint.schedule.duration_days = duration_days + return constraint + def update_constraint_endpoint_location( constraints, endpoint_id : EndPointId, region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None, @@ -221,6 +237,23 @@ def update_constraint_sla_isolation( constraint.sla_isolation.isolation_level.append(isolation_level) return constraint +def update_constraint_qos_profile( + constraints, qos_profile_id : QoSProfileId, qos_profile_name : str, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: + for constraint in constraints: + if constraint.WhichOneof('constraint') != 'qos_profile': continue + break # found, end loop + else: + # not found, add it + constraint = constraints.add() # pylint: disable=no-member + + constraint.action = new_action + + constraint.qos_profile.qos_profile_id.qos_profile_id.uuid = qos_profile_id.qos_profile_id.uuid + constraint.qos_profile.qos_profile_name = qos_profile_name + return constraint + def copy_constraints(source_constraints, target_constraints): for source_constraint in source_constraints: constraint_kind = source_constraint.WhichOneof('constraint') @@ -240,6 +273,11 @@ def copy_constraints(source_constraints, target_constraints): update_constraint_custom_scalar( target_constraints, constraint_type, constraint_value, raise_if_differs=raise_if_differs) + elif constraint_kind == 'schedule': + start_timestamp = source_constraint.schedule.start_timestamp + duration_days = source_constraint.schedule.duration_days + update_constraint_schedule(target_constraints, start_timestamp, duration_days) + elif constraint_kind == 'endpoint_location': endpoint_id = source_constraint.endpoint_location.endpoint_id location = source_constraint.endpoint_location.location @@ -282,5 +320,10 @@ def copy_constraints(source_constraints, target_constraints): isolation_levels = sla_isolation.isolation_level update_constraint_sla_isolation(target_constraints, isolation_levels) + elif constraint_kind == 'qos_profile': + qos_profile_id = source_constraint.qos_profile.qos_profile_id + qos_profile_name = source_constraint.qos_profile.qos_profile_name + update_constraint_qos_profile(target_constraints, qos_profile_id, qos_profile_name) + else: raise NotImplementedError('Constraint({:s})'.format(grpc_message_to_json_string(source_constraint))) diff --git a/src/common/tools/object_factory/ConfigRule.py b/src/common/tools/object_factory/ConfigRule.py index 9ba7066bd..df3d24eba 100644 --- a/src/common/tools/object_factory/ConfigRule.py +++ b/src/common/tools/object_factory/ConfigRule.py @@ -16,12 +16,14 @@ import json from typing import Any, Dict, Union from common.proto.context_pb2 import ConfigActionEnum -def json_config_rule(action : ConfigActionEnum, resource_key : str, resource_value : Union[str, Dict[str, Any]]): +def json_config_rule( + action : ConfigActionEnum, resource_key : str, resource_value : Union[str, Dict[str, Any]] +) -> Dict: if not isinstance(resource_value, str): resource_value = json.dumps(resource_value, sort_keys=True) return {'action': action, 'custom': {'resource_key': resource_key, 'resource_value': resource_value}} -def json_config_rule_set(resource_key : str, resource_value : Union[str, Dict[str, Any]]): +def json_config_rule_set(resource_key : str, resource_value : Union[str, Dict[str, Any]]) -> Dict: return json_config_rule(ConfigActionEnum.CONFIGACTION_SET, resource_key, resource_value) -def json_config_rule_delete(resource_key : str, resource_value : Union[str, Dict[str, Any]]): +def json_config_rule_delete(resource_key : str, resource_value : Union[str, Dict[str, Any]]) -> Dict: return json_config_rule(ConfigActionEnum.CONFIGACTION_DELETE, resource_key, resource_value) diff --git a/src/common/tools/object_factory/Constraint.py b/src/common/tools/object_factory/Constraint.py index dd7ed9307..112441fec 100644 --- a/src/common/tools/object_factory/Constraint.py +++ b/src/common/tools/object_factory/Constraint.py @@ -15,44 +15,96 @@ import json from typing import Any, Dict, List, Union -def json_constraint_custom(constraint_type : str, constraint_value : Union[str, Dict[str, Any]]) -> Dict: +from common.proto.context_pb2 import ConstraintActionEnum + + +def json_constraint_custom( + constraint_type : str, constraint_value : Union[str, Dict[str, Any]], + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: if not isinstance(constraint_value, str): constraint_value = json.dumps(constraint_value, sort_keys=True) - return {'custom': {'constraint_type': constraint_type, 'constraint_value': constraint_value}} + return {'action': action, 'custom': { + 'constraint_type': constraint_type, 'constraint_value': constraint_value + }} -def json_constraint_schedule(start_timestamp : float, duration_days : float) -> Dict: - return {'schedule': {'start_timestamp': start_timestamp, 'duration_days': duration_days}} +def json_constraint_schedule( + start_timestamp : float, duration_days : float, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'schedule': { + 'start_timestamp': start_timestamp, 'duration_days': duration_days + }} -def json_constraint_endpoint_location_region(endpoint_id : Dict, region : str) -> Dict: - return {'endpoint_location': {'endpoint_id': endpoint_id, 'location': {'region': region}}} +def json_constraint_endpoint_location_region( + endpoint_id : Dict, region : str, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'endpoint_location': { + 'endpoint_id': endpoint_id, 'location': {'region': region} + }} -def json_constraint_endpoint_location_gps(endpoint_id : Dict, latitude : float, longitude : float) -> Dict: +def json_constraint_endpoint_location_gps( + endpoint_id : Dict, latitude : float, longitude : float, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: gps_position = {'latitude': latitude, 'longitude': longitude} - return {'endpoint_location': {'endpoint_id': endpoint_id, 'location': {'gps_position': gps_position}}} + return {'action': action, 'endpoint_location': { + 'endpoint_id': endpoint_id, 'location': {'gps_position': gps_position} + }} -def json_constraint_endpoint_priority(endpoint_id : Dict, priority : int) -> Dict: - return {'endpoint_priority': {'endpoint_id': endpoint_id, 'priority': priority}} +def json_constraint_endpoint_priority( + endpoint_id : Dict, priority : int, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'endpoint_priority': { + 'endpoint_id': endpoint_id, 'priority': priority + }} -def json_constraint_sla_capacity(capacity_gbps : float) -> Dict: - return {'sla_capacity': {'capacity_gbps': capacity_gbps}} +def json_constraint_sla_capacity( + capacity_gbps : float, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'sla_capacity': { + 'capacity_gbps': capacity_gbps + }} -def json_constraint_sla_latency(e2e_latency_ms : float) -> Dict: - return {'sla_latency': {'e2e_latency_ms': e2e_latency_ms}} +def json_constraint_sla_latency( + e2e_latency_ms : float, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'sla_latency': { + 'e2e_latency_ms': e2e_latency_ms + }} -def json_constraint_sla_availability(num_disjoint_paths : int, all_active : bool, availability : float) -> Dict: - return {'sla_availability': { +def json_constraint_sla_availability( + num_disjoint_paths : int, all_active : bool, availability : float, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'sla_availability': { 'num_disjoint_paths': num_disjoint_paths, 'all_active': all_active, 'availability': availability }} -def json_constraint_sla_isolation(isolation_levels : List[int]) -> Dict: - return {'sla_isolation': {'isolation_level': isolation_levels}} +def json_constraint_sla_isolation( + isolation_levels : List[int], + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'sla_isolation': { + 'isolation_level': isolation_levels + }} def json_constraint_exclusions( is_permanent : bool = False, device_ids : List[Dict] = [], endpoint_ids : List[Dict] = [], - link_ids : List[Dict] = [] + link_ids : List[Dict] = [], + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Dict: + return {'action': action, 'exclusions': { + 'is_permanent': is_permanent, 'device_ids': device_ids, 'endpoint_ids': endpoint_ids, 'link_ids': link_ids + }} + +def json_constraint_qos_profile( + qos_profile_id : Dict, qos_profile_name : int, + action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Dict: - return {'exclusions': { - 'is_permanent' : is_permanent, - 'device_ids' : device_ids, - 'endpoint_ids' : endpoint_ids, - 'link_ids' : link_ids, + return {'action': action, 'qos_profile': { + 'qos_profile_id': qos_profile_id, 'qos_profile_name': qos_profile_name }} -- GitLab From de40a8650cd8518f8704b92ce9c1e89f0b02e602 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 14:55:13 +0000 Subject: [PATCH 16/19] Tests - Mock TFS NBI Dependencies: - Added QoS Profile mock service --- src/tests/tools/mock_tfs_nbi_dependencies/Config.py | 1 + .../MockService_Dependencies.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tests/tools/mock_tfs_nbi_dependencies/Config.py b/src/tests/tools/mock_tfs_nbi_dependencies/Config.py index 621bc3187..146fd3a99 100644 --- a/src/tests/tools/mock_tfs_nbi_dependencies/Config.py +++ b/src/tests/tools/mock_tfs_nbi_dependencies/Config.py @@ -29,6 +29,7 @@ LOG_LEVEL = str(get_log_level()) MOCKED_SERVICES = [ ServiceNameEnum.CONTEXT, ServiceNameEnum.DEVICE, + ServiceNameEnum.QOSPROFILE, ServiceNameEnum.SERVICE, ServiceNameEnum.SLICE, ] diff --git a/src/tests/tools/mock_tfs_nbi_dependencies/MockService_Dependencies.py b/src/tests/tools/mock_tfs_nbi_dependencies/MockService_Dependencies.py index 74ef6bdad..f8a57154a 100644 --- a/src/tests/tools/mock_tfs_nbi_dependencies/MockService_Dependencies.py +++ b/src/tests/tools/mock_tfs_nbi_dependencies/MockService_Dependencies.py @@ -15,18 +15,21 @@ from typing import Optional, Union from common.proto.context_pb2_grpc import add_ContextServiceServicer_to_server from common.proto.device_pb2_grpc import add_DeviceServiceServicer_to_server +from common.proto.qos_profile_pb2_grpc import add_QoSProfileServiceServicer_to_server from common.proto.service_pb2_grpc import add_ServiceServiceServicer_to_server from common.proto.slice_pb2_grpc import add_SliceServiceServicer_to_server from common.tests.MockServicerImpl_Context import MockServicerImpl_Context from common.tests.MockServicerImpl_Device import MockServicerImpl_Device +from common.tests.MockServicerImpl_QoSProfile import MockServicerImpl_QoSProfile from common.tests.MockServicerImpl_Service import MockServicerImpl_Service from common.tests.MockServicerImpl_Slice import MockServicerImpl_Slice from common.tools.service.GenericGrpcService import GenericGrpcService class MockService_Dependencies(GenericGrpcService): - # Mock Service implementing Mock Context, Device, Service and Slice to - # simplify unitary tests of the NBI component. + # Mock Service implementing multiple mock components to simplify + # unitary tests of the NBI component. + # Mocks implemented: Context, Device, QoS Profile, Service and Slice def __init__( self, bind_port : Union[str, int], bind_address : Optional[str] = None, @@ -53,3 +56,6 @@ class MockService_Dependencies(GenericGrpcService): self.slice_servicer = MockServicerImpl_Slice() add_SliceServiceServicer_to_server(self.slice_servicer, self.server) + + self.qos_profile_servicer = MockServicerImpl_QoSProfile() + add_QoSProfileServiceServicer_to_server(self.qos_profile_servicer, self.server) -- GitLab From 1446f0c9c8950008023db7649ee22e195b6db137 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 14:59:02 +0000 Subject: [PATCH 17/19] NBI Component: - Corrected unitary tests - Removed unneeded scripts - Implemented Unitary test for Camara NBI - Corrected logic for CAMARA NBI - Added missing HTTP Status Codes - Added CAMARA NBI to GitLab CI/CD tests --- scripts/run_tests_locally-nbi-all.sh | 26 +- scripts/run_tests_locally-nbi-camara-qod.sh | 30 -- scripts/run_tests_locally-nbi-core.sh | 25 - scripts/run_tests_locally-nbi-etsi-bwm.sh | 25 - scripts/run_tests_locally-nbi-ietf-l2vpn.sh | 25 - scripts/run_tests_locally-nbi-ietf-l3vpn.sh | 25 - scripts/run_tests_locally-nbi-ietf-network.sh | 25 - scripts/run_tests_locally-nbi-ietf-slice.sh | 25 - scripts/run_tests_locally-nbi-tfs-api.sh | 25 - src/nbi/.gitlab-ci.yml | 2 +- src/nbi/service/_tools/HttpStatusCodes.py | 5 +- src/nbi/service/camara_qod/Resources.py | 432 +++++++++++------- src/nbi/service/camara_qod/Tools.py | 304 ++++++------ src/nbi/service/camara_qod/__init__.py | 6 +- src/nbi/tests/MockService_Dependencies.py | 77 ---- src/nbi/tests/PrepareTestScenario.py | 88 +--- src/nbi/tests/test_camara_qod.py | 114 ++++- src/nbi/tests/test_core.py | 1 - src/nbi/tests/test_etsi_bwm.py | 1 - src/nbi/tests/test_ietf_l2vpn.py | 1 - src/nbi/tests/test_ietf_l3vpn.py | 1 - src/nbi/tests/test_ietf_network.py | 1 - src/nbi/tests/test_tfs_api.py | 1 - 23 files changed, 540 insertions(+), 725 deletions(-) delete mode 100755 scripts/run_tests_locally-nbi-camara-qod.sh delete mode 100755 scripts/run_tests_locally-nbi-core.sh delete mode 100755 scripts/run_tests_locally-nbi-etsi-bwm.sh delete mode 100755 scripts/run_tests_locally-nbi-ietf-l2vpn.sh delete mode 100755 scripts/run_tests_locally-nbi-ietf-l3vpn.sh delete mode 100755 scripts/run_tests_locally-nbi-ietf-network.sh delete mode 100755 scripts/run_tests_locally-nbi-ietf-slice.sh delete mode 100755 scripts/run_tests_locally-nbi-tfs-api.sh delete mode 100644 src/nbi/tests/MockService_Dependencies.py diff --git a/scripts/run_tests_locally-nbi-all.sh b/scripts/run_tests_locally-nbi-all.sh index a8b01b31a..81bff623b 100755 --- a/scripts/run_tests_locally-nbi-all.sh +++ b/scripts/run_tests_locally-nbi-all.sh @@ -47,9 +47,7 @@ KAFKA_IP=$(docker inspect kafka --format "{{.NetworkSettings.Networks.teraflowbr echo "Kafka IP: $KAFKA_IP" docker run --name mock_tfs_nbi_dependencies -d -p 10000:10000 \ - --network=teraflowbridge \ - --env BIND_ADDRESS=0.0.0.0 \ - --env BIND_PORT=10000 \ + --network=teraflowbridge --env BIND_ADDRESS=0.0.0.0 --env BIND_PORT=10000 \ --env LOG_LEVEL=INFO \ mock_tfs_nbi_dependencies:test @@ -69,23 +67,25 @@ printf "\n" sleep 5 # Give extra time to NBI to get ready docker ps -a -docker logs kafka +#docker logs kafka docker logs mock_tfs_nbi_dependencies docker logs nbi # helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_core.py --junitxml=/opt/results/${IMAGE_NAME}_report_core.xml" -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_tfs_api.py --junitxml=/opt/results/${IMAGE_NAME}_report_tfs_api.xml" -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_l2vpn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_l2vpn.xml" -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_network.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_network.xml" -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_l3vpn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_l3vpn.xml" -docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_etsi_bwm.py --junitxml=/opt/results/${IMAGE_NAME}_report_etsi_bwm.xml" -docker exec -i nbi bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_core.py --junitxml=/opt/results/nbi_report_core.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_tfs_api.py --junitxml=/opt/results/nbi_report_tfs_api.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_l2vpn.py --junitxml=/opt/results/nbi_report_ietf_l2vpn.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_network.py --junitxml=/opt/results/nbi_report_ietf_network.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_ietf_l3vpn.py --junitxml=/opt/results/nbi_report_ietf_l3vpn.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_etsi_bwm.py --junitxml=/opt/results/nbi_report_etsi_bwm.xml" +docker exec -i nbi bash -c "coverage run --append -m pytest --log-level=INFO --verbose nbi/tests/test_camara_qod.py --junitxml=/opt/results/nbi_report_camara_qod.xml" +docker exec -i nbi bash -c "coverage report --include='nbi/*' --show-missing" #docker logs mock_tfs_nbi_dependencies -#docker logs nbi +docker logs nbi #docker logs kafka -docker rm -f mock_tfs_nbi_dependencies nbi +docker rm -f nbi +docker rm -f mock_tfs_nbi_dependencies docker rm -f kafka docker network rm teraflowbridge diff --git a/scripts/run_tests_locally-nbi-camara-qod.sh b/scripts/run_tests_locally-nbi-camara-qod.sh deleted file mode 100755 index 894e739ac..000000000 --- a/scripts/run_tests_locally-nbi-camara-qod.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 ETSI OSG/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. - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc -COVERAGEFILE=$PROJECTDIR/coverage/.coverage - -# Destroy old coverage file and configure the correct folder on the .coveragerc file -rm -f $COVERAGEFILE -cat $PROJECTDIR/coverage/.coveragerc.template | sed s+~/tfs-ctrl+$PROJECTDIR+g > $RCFILE - - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_camara_qod.py diff --git a/scripts/run_tests_locally-nbi-core.sh b/scripts/run_tests_locally-nbi-core.sh deleted file mode 100755 index e6eb06a62..000000000 --- a/scripts/run_tests_locally-nbi-core.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_core.py diff --git a/scripts/run_tests_locally-nbi-etsi-bwm.sh b/scripts/run_tests_locally-nbi-etsi-bwm.sh deleted file mode 100755 index a335fbe4f..000000000 --- a/scripts/run_tests_locally-nbi-etsi-bwm.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_etsi_bwm.py diff --git a/scripts/run_tests_locally-nbi-ietf-l2vpn.sh b/scripts/run_tests_locally-nbi-ietf-l2vpn.sh deleted file mode 100755 index 19556ddf0..000000000 --- a/scripts/run_tests_locally-nbi-ietf-l2vpn.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_ietf_l2vpn.py diff --git a/scripts/run_tests_locally-nbi-ietf-l3vpn.sh b/scripts/run_tests_locally-nbi-ietf-l3vpn.sh deleted file mode 100755 index 01cf5d975..000000000 --- a/scripts/run_tests_locally-nbi-ietf-l3vpn.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_ietf_l3vpn.py diff --git a/scripts/run_tests_locally-nbi-ietf-network.sh b/scripts/run_tests_locally-nbi-ietf-network.sh deleted file mode 100755 index 401e2615c..000000000 --- a/scripts/run_tests_locally-nbi-ietf-network.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_ietf_network.py diff --git a/scripts/run_tests_locally-nbi-ietf-slice.sh b/scripts/run_tests_locally-nbi-ietf-slice.sh deleted file mode 100755 index bf53f18b9..000000000 --- a/scripts/run_tests_locally-nbi-ietf-slice.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 ETSI OSG/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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_slice_2.py diff --git a/scripts/run_tests_locally-nbi-tfs-api.sh b/scripts/run_tests_locally-nbi-tfs-api.sh deleted file mode 100755 index e27faa8c2..000000000 --- a/scripts/run_tests_locally-nbi-tfs-api.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copyright 2022-2024 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. - - -PROJECTDIR=`pwd` - -cd $PROJECTDIR/src -RCFILE=$PROJECTDIR/coverage/.coveragerc - -# Run unitary tests and analyze coverage of code at same time -# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 -coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ - nbi/tests/test_tfs_api.py diff --git a/src/nbi/.gitlab-ci.yml b/src/nbi/.gitlab-ci.yml index 938736dd9..8456844b7 100644 --- a/src/nbi/.gitlab-ci.yml +++ b/src/nbi/.gitlab-ci.yml @@ -115,7 +115,7 @@ unit_test nbi: - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_network.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_network.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_ietf_l3vpn.py --junitxml=/opt/results/${IMAGE_NAME}_report_ietf_l3vpn.xml" - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_etsi_bwm.py --junitxml=/opt/results/${IMAGE_NAME}_report_etsi_bwm.xml" - #- docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_camara_qod.py --junitxml=/opt/results/${IMAGE_NAME}_report_camara_qod.xml" + - docker exec -i $IMAGE_NAME bash -c "coverage run --append -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_camara_qod.py --junitxml=/opt/results/${IMAGE_NAME}_report_camara_qod.xml" - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' after_script: diff --git a/src/nbi/service/_tools/HttpStatusCodes.py b/src/nbi/service/_tools/HttpStatusCodes.py index 56ea475c7..811cf4e75 100644 --- a/src/nbi/service/_tools/HttpStatusCodes.py +++ b/src/nbi/service/_tools/HttpStatusCodes.py @@ -14,7 +14,10 @@ HTTP_OK = 200 HTTP_CREATED = 201 +HTTP_ACCEPTED = 202 HTTP_NOCONTENT = 204 HTTP_BADREQUEST = 400 +HTTP_NOTFOUND = 404 +HTTP_UNSUPMEDIATYPE = 415 HTTP_SERVERERROR = 500 -HTTP_GATEWAYTIMEOUT = 504 \ No newline at end of file +HTTP_GATEWAYTIMEOUT = 504 diff --git a/src/nbi/service/camara_qod/Resources.py b/src/nbi/service/camara_qod/Resources.py index e2ecf8729..a33d7942f 100644 --- a/src/nbi/service/camara_qod/Resources.py +++ b/src/nbi/service/camara_qod/Resources.py @@ -16,17 +16,21 @@ import copy, grpc, grpc._channel, logging from typing import Dict from uuid import uuid4 -from flask.json import jsonify from flask_restful import Resource, request from common.proto.context_pb2 import Empty, QoSProfileId, Uuid from common.Constants import DEFAULT_CONTEXT_NAME +from common.tools.context_queries.Service import get_service_by_uuid +from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from qos_profile.client.QoSProfileClient import QoSProfileClient from service.client.ServiceClient import ServiceClient from .Tools import ( - format_grpc_to_json, grpc_context_id, grpc_service_id, - QOD_2_service, service_2_qod, grpc_message_to_qos_table_data, - create_qos_profile_from_json + create_qos_profile_from_json, grpc_context_id, grpc_service_id, + grpc_message_to_qos_table_data, QOD_2_service, service_2_qod +) +from nbi.service._tools.HttpStatusCodes import ( + HTTP_ACCEPTED, HTTP_BADREQUEST, HTTP_CREATED, HTTP_NOCONTENT, HTTP_NOTFOUND, + HTTP_OK, HTTP_SERVERERROR, HTTP_UNSUPMEDIATYPE ) LOGGER = logging.getLogger(__name__) @@ -34,187 +38,285 @@ LOGGER = logging.getLogger(__name__) class _Resource(Resource): def __init__(self) -> None: super().__init__() + self.context_client = ContextClient() self.qos_profile_client = QoSProfileClient() - self.client = ContextClient() - self.service_client = ServiceClient() + self.service_client = ServiceClient() + +def compose_error(msg_error, http_status_code): + LOGGER.exception(msg_error) + return {"error": msg_error}, http_status_code + +def compose_internal_server_error(msg_error): + return compose_error(msg_error, HTTP_SERVERERROR) + +def compose_bad_request_error(msg_error): + return compose_error(msg_error, HTTP_BADREQUEST) + +def compose_not_found_error(msg_error): + return compose_error(msg_error, HTTP_NOTFOUND) + +def compose_unsupported_media_type_error(): + msg_error = "JSON payload is required to proceed" + return compose_error(msg_error, HTTP_UNSUPMEDIATYPE) + + + +##### PROFILES ######################################################################################################### -#ProfileList Endpoint for posting class ProfileList(_Resource): def post(self): - if not request.is_json: - return {"message": "JSON payload is required to proceed"}, 415 - request_data: Dict = request.get_json() #get the json from the test function - request_data['qos_profile_id']=str(uuid4()) # Create qos ID randomly using uuid4 - # JSON TO GRPC to store the data in the grpc server + if not request.is_json: return compose_unsupported_media_type_error() + + request_data : Dict = request.get_json() + request_data_with_id = copy.deepcopy(request_data) + request_data_with_id["qos_profile_id"] = str(uuid4()) + try: - qos_profile = create_qos_profile_from_json(request_data) - except: - LOGGER.exception("Error happened while creating QoS profile from json") - return {"message": "Failed to create QoS profile"}, 500 - # Send to gRPC server using CreateQosProfile + qos_profile = create_qos_profile_from_json(request_data_with_id) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error parsing QoSProfile({:s})".format(str(request_data)) + ) + try: qos_profile_created = self.qos_profile_client.CreateQoSProfile(qos_profile) - except: - LOGGER.exception("error happened while creating QoS profile") - #gRPC message back to JSON using the helper function - qos_profile_data = grpc_message_to_qos_table_data(qos_profile_created) - LOGGER.info(f'qos_profile_data{qos_profile_data}') - return jsonify(qos_profile_data) - + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error creating QoSProfile({:s}) QoSProfileWithUuid({:s})".format( + str(request_data), str(request_data_with_id) + ) + ) + + return grpc_message_to_qos_table_data(qos_profile_created), HTTP_CREATED + def get(self): - qos_profiles = self.qos_profile_client.GetQoSProfiles(Empty()) #get all of type empty and defined in .proto file () - qos_profile_list = [] #since it iterates over QoSProfile , create a list to store all QOS profiles - for qos_profile in qos_profiles: - LOGGER.info('qos_profiles = {:s}'.format(str(qos_profiles))) - qos_profile_data = grpc_message_to_qos_table_data(qos_profile) #transform to json - qos_profile_list.append(qos_profile_data) # append to the list - - return jsonify(qos_profile_list) - -#getting,updating,deleting using the qos profile id + list_qos_profiles = self.qos_profile_client.GetQoSProfiles(Empty()) + list_qos_profiles = [ + grpc_message_to_qos_table_data(qos_profile) + for qos_profile in list_qos_profiles + ] + return list_qos_profiles, HTTP_OK + class ProfileDetail(_Resource): - def get(self, qos_profile_id): - id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) #this is because we want to GetQOSProfile which takes QOSProfileID - #or - #id=QoSProfileId() - #id.qos_profile_id.uuid=qos_profile_id - - #The QoSProfileID is message qod_profile_id of type Uuid - # Uuid is a message uuid of type qos_profile_id which is a string + def get(self, qos_profile_id : str): + _qos_profile_id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + try: - qos_profile = self.qos_profile_client.GetQoSProfile(id) #get the qosprofile from grpc server according to ID - qos_profile_data = grpc_message_to_qos_table_data(qos_profile) # grpc to json agian to view it on http - return jsonify(qos_profile_data) - except grpc._channel._InactiveRpcError as exc: - if exc.code() == grpc.StatusCode.NOT_FOUND: - LOGGER.warning(f"QoSProfile not found: {qos_profile_id}") - return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - LOGGER.exception("gRPC error while fetching QoSProfile") - return {"error": "Internal Server Error"}, 500 - except: - LOGGER.exception("Error while fetching QoSProfile") - return {"error": "Internal Server Error"}, 500 - - def put(self, qos_profile_id): + qos_profile = self.qos_profile_client.GetQoSProfile(_qos_profile_id) + return grpc_message_to_qos_table_data(qos_profile), HTTP_OK + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoSProfileId({:s}) not found".format(str(qos_profile_id)) + ) + else: + return compose_internal_server_error( + "gRPC error fetching QoSProfileId({:s})".format(str(qos_profile_id)) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error fetching QoSProfileId({:s})".format(str(qos_profile_id)) + ) + + def put(self, qos_profile_id : str): + if not request.is_json: return compose_unsupported_media_type_error() + + request_data : Dict = request.get_json() + request_data_orig = copy.deepcopy(request_data) + + if "qos_profile_id" in request_data: + if request_data["qos_profile_id"] != qos_profile_id: + return compose_bad_request_error( + "qos_profile_id({:s}) in JSON payload mismatches qos_profile_id({:s}) in URL".format( + str(request_data["qos_profile_id"]), str(qos_profile_id) + ) + ) + else: + request_data["qos_profile_id"] = qos_profile_id + try: - request_data = request.get_json() # get the json to do the update - if 'qos_profile_id' not in request_data: #ensuring the update on qos profile id - request_data['qos_profile_id'] = qos_profile_id # ID to be updated - qos_profile = create_qos_profile_from_json(request_data) # Transform it again to grpc - qos_profile_updated = self.qos_profile_client.UpdateQoSProfile(qos_profile) # update the profile in the grpc server - return grpc_message_to_qos_table_data(qos_profile_updated), 200 - except KeyError as e: - LOGGER.exception("Missing required key") - return {"error": f"Missing required key: {str(e)}"}, 400 - except grpc._channel._InactiveRpcError as exc: - if exc.code() == grpc.StatusCode.NOT_FOUND: - LOGGER.warning(f"QoSProfile not found for update: {qos_profile_id}") - return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - LOGGER.exception("gRPC error while updating QoSProfile") - return {"error": "Internal Server Error"}, 500 - except: - LOGGER.exception(f"Error in PUT /profiles/{qos_profile_id}") - return {"error": "Internal Server Error"}, 500 - - def delete(self, qos_profile_id): - id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) # get id to delete accordingly + qos_profile = create_qos_profile_from_json(request_data) + qos_profile_updated = self.qos_profile_client.UpdateQoSProfile(qos_profile) + return grpc_message_to_qos_table_data(qos_profile_updated), HTTP_ACCEPTED + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoSProfileId({:s}) not found".format(str(qos_profile_id)) + ) + else: + return compose_internal_server_error( + "gRPC error updating QoSProfileId({:s}) with content QosProfile({:s})".format( + str(qos_profile_id), str(request_data_orig) + ) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error updating QoSProfileId({:s}) with content QosProfile({:s})".format( + str(qos_profile_id), str(request_data_orig) + ) + ) + + def delete(self, qos_profile_id : str): + _qos_profile_id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + try: - qos_profile = self.qos_profile_client.DeleteQoSProfile(id) - qos_profile_data = grpc_message_to_qos_table_data(qos_profile) - return jsonify(qos_profile_data) - except grpc._channel._InactiveRpcError as exc: - if exc.code() == grpc.StatusCode.NOT_FOUND: - LOGGER.warning(f"QoSProfile not found for deletion: {qos_profile_id}") - return {"error": f"QoSProfile {qos_profile_id} not found"}, 404 - LOGGER.error(f"gRPC error while deleting QoSProfile: {exc}") - return {"error": "Internal Server Error"}, 500 - except: - LOGGER.exception(f"Error in DELETE /profiles/{qos_profile_id}") - return {"error": "Internal Server Error"}, 500 - -### SESSION ########################################################## + self.qos_profile_client.DeleteQoSProfile(_qos_profile_id) + return {}, HTTP_NOCONTENT + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoSProfileId({:s}) not found".format(str(qos_profile_id)) + ) + else: + return compose_internal_server_error( + "gRPC error deleting QoSProfileId({:s})".format(str(qos_profile_id)) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error deleting QoSProfileId({:s})".format(str(qos_profile_id)) + ) + + +##### SESSIONS ######################################################################################################### class QodInfo(_Resource): def post(self): - if not request.is_json: - return jsonify({'error': 'Unsupported Media Type', 'message': 'JSON payload is required'}), 415 - request_data: Dict = request.get_json() - qos_profile_id = request_data.get('qos_profile_id') - qos_session_id = request_data.get('qos_session_id') - duration = request_data.get('duration') - LOGGER.info(f'qos_profile_id:{qos_profile_id}') - if not qos_profile_id: - return jsonify({'error': 'qos_profile_id is required'}), 400 - if qos_session_id: - return jsonify({'error': 'qos_session_id is not allowed in creation'}), 400 - service = QOD_2_service(self.client, request_data, qos_profile_id, duration) + if not request.is_json: return compose_unsupported_media_type_error() + + request_data : Dict = request.get_json() + request_data_orig = copy.deepcopy(request_data) + + session_id = request_data.get("session_id") + if session_id is not None: + return compose_bad_request_error("session_id is not allowed in creation") + + qos_profile_id = request_data.get("qos_profile_id") + if qos_profile_id is None: + return compose_bad_request_error("qos_profile_id is required") + + try: + service = QOD_2_service(self.context_client, self.qos_profile_client, request_data) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error parsing QoDSession({:s})".format(str(request_data_orig)) + ) + stripped_service = copy.deepcopy(service) - stripped_service.ClearField('service_endpoint_ids') - stripped_service.ClearField('service_constraints') - stripped_service.ClearField('service_config') + stripped_service.ClearField("service_endpoint_ids") + stripped_service.ClearField("service_constraints") + stripped_service.ClearField("service_config") try: - create_response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) - update_response = format_grpc_to_json(self.service_client.UpdateService(service)) - response = { - "create_response": create_response, - "update_response": update_response - } - except Exception as e: - LOGGER.error(f"Unexpected error: {str(e)}", exc_info=True) - return jsonify({'error': 'Internal Server Error', 'message': 'An unexpected error occurred'}), 500 - return response - #didnt work return jsonify(response) + self.service_client.CreateService(stripped_service) + self.service_client.UpdateService(service) + + service_uuid = service.service_id.service_uuid.uuid + updated_service = get_service_by_uuid(self.context_client, service_uuid, rw_copy=False) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error creating Service({:s}) for QoDSession({:s})".format( + grpc_message_to_json_string(service), str(request_data_orig) + ) + ) + + return service_2_qod(updated_service), HTTP_CREATED def get(self): - service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) #return context id as json - qod_info = [service_2_qod(service) for service in service_list.services] #iterating over service list - LOGGER.info(f"error related to qod_info: {qod_info}") - return qod_info - #didnt work return jsonify(qod_info) + list_services = self.context_client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME)) + list_services = [service_2_qod(service) for service in list_services.services] + return list_services, HTTP_OK + class QodInfoID(_Resource): - def get(self, sessionId: str): + def get(self, session_id: str): + try: + service = get_service_by_uuid(self.context_client, session_id, rw_copy=True) + return service_2_qod(service), HTTP_OK + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoDSessionId({:s}) not found".format(str(session_id)) + ) + else: + return compose_internal_server_error( + "gRPC error fetching QoDSessionId({:s})".format(str(session_id)) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error fetching QoDSessionId({:s})".format(str(session_id)) + ) + + def put(self, session_id : str): + if not request.is_json: return compose_unsupported_media_type_error() + + request_data : Dict = request.get_json() + request_data_orig = copy.deepcopy(request_data) + + if "session_id" in request_data: + if request_data["session_id"] != session_id: + return compose_bad_request_error( + "session_id({:s}) in JSON payload mismatches session_id({:s}) in URL".format( + str(request_data["session_id"]), str(session_id) + ) + ) + else: + request_data["session_id"] = session_id + + qos_profile_id = request_data.get("qos_profile_id") + if qos_profile_id is None: + return compose_bad_request_error("qos_profile_id is required") + + duration = request_data.get("duration") + if duration is None: + return compose_bad_request_error("duration is required") + try: - service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) - return service_2_qod(service) - except grpc._channel._InactiveRpcError as exc: - if exc.code()==grpc.StatusCode.NOT_FOUND: - LOGGER.warning(f"Qod Session not found: {sessionId}") - return {"error": f"Qod Session {sessionId} not found"}, 404 - - def put(self, sessionId: str): + service = get_service_by_uuid(self.context_client, session_id, rw_copy=True) + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoDSessionId({:s}) not found".format(str(session_id)) + ) + else: + return compose_internal_server_error( + "gRPC error fetching QoDSessionId({:s})".format(str(session_id)) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error fetching QoDSessionId({:s})".format(str(session_id)) + ) + + for constraint in service.service_constraints: + if constraint.WhichOneof("constraint") == "schedule": + constraint.schedule.duration_days = duration + + try: + self.service_client.UpdateService(service) + + service_uuid = service.service_id.service_uuid.uuid + updated_service = get_service_by_uuid(self.context_client, service_uuid, rw_copy=False) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error updating Service({:s}) for QoDSession({:s})".format( + grpc_message_to_json_string(service), str(request_data_orig) + ) + ) + + return service_2_qod(updated_service), HTTP_ACCEPTED + + def delete(self, session_id: str): try: - request_data: Dict = request.get_json() - session_id = request_data.get('session_id') - if not session_id: - return jsonify({'error': 'sessionId is required'}), 400 - qos_profile_id = request_data.get('qos_profile_id') - if not qos_profile_id: - return jsonify({'error': 'qos_profile_id is required'}), 400 - duration = request_data.get('duration') - service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) #to get service we should have the context and the session id - if qos_profile_id: - service.name = qos_profile_id # if we provide a new qos profile , update the service name with new qos_profile_id - if duration: - for constraint in service.service_constraints: - if constraint.WhichOneof('constraint') == 'schedule': - constraint.schedule.duration_days = duration - updated_service = self.service_client.UpdateService(service) - qod_response = service_2_qod(updated_service) - return qod_response, 200 - except KeyError as e: - LOGGER.error(f"Missing required key: {e}") - return {"error": f"Missing required key: {str(e)}"}, 400 - except grpc._channel._InactiveRpcError as exc: - if exc.code() == grpc.StatusCode.NOT_FOUND: - LOGGER.warning(f"Qod Session not found: {sessionId}") - return {"error": f"Qod Session {sessionId} not found"}, 404 - LOGGER.error(f"gRPC error while updating Qod Session: {exc}") - return {"error": "Internal Server Error"}, 500 - except: - LOGGER.exception(f"Error in PUT /sessions/{sessionId}") - return {"error": "Internal Server Error"}, 500 - - def delete(self, sessionId: str): - self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, sessionId)) - return{"Session Deleted"} + self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, session_id)) + return {}, HTTP_NOCONTENT + except grpc._channel._InactiveRpcError as e: + if e.code() == grpc.StatusCode.NOT_FOUND: + return compose_not_found_error( + "QoDSessionId({:s}) not found".format(str(session_id)) + ) + else: + return compose_internal_server_error( + "gRPC error deleting QoDSessionId({:s})".format(str(session_id)) + ) + except: # pylint: disable=bare-except + return compose_internal_server_error( + "Error deleting QoDSessionId({:s})".format(str(session_id)) + ) diff --git a/src/nbi/service/camara_qod/Tools.py b/src/nbi/service/camara_qod/Tools.py index bc63e43d7..f0ac60003 100644 --- a/src/nbi/service/camara_qod/Tools.py +++ b/src/nbi/service/camara_qod/Tools.py @@ -13,18 +13,20 @@ # limitations under the License. import json, logging, re, time -from flask.json import jsonify from netaddr import IPAddress, IPNetwork +from typing import Dict, Tuple from uuid import uuid4 +from common.Constants import DEFAULT_CONTEXT_NAME from common.proto.context_pb2 import ( - ConfigActionEnum, ConfigRule, ConfigRule_Custom, ContextId, Empty, EndPointId, - QoSProfileId, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum, Uuid + ContextId, Empty, EndPointId, QoSProfileId, Service, ServiceId, + ServiceStatusEnum, ServiceTypeEnum, Uuid ) from common.proto.qos_profile_pb2 import ( QoSProfileValueUnitPair, QoSProfile,QoDConstraintsRequest ) from common.tools.grpc.ConfigRules import update_config_rule_custom -from common.tools.grpc.Tools import grpc_message_to_json +from common.tools.grpc.Constraints import copy_constraints +from common.tools.grpc.Tools import grpc_message_to_json, grpc_message_to_json_string from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Service import json_service_id from context.client.ContextClient import ContextClient @@ -33,194 +35,164 @@ from qos_profile.client.QoSProfileClient import QoSProfileClient LOGGER = logging.getLogger(__name__) -ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' -DEVICE_SETTINGS_KEY = '/device[{:s}]/settings' -RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') -MEC_CONSIDERED_FIELDS = [ - 'device', 'applicationServer', 'qosProfile', 'sessionId', 'duration', - 'startedAt', 'expiresAt', 'qosStatus' +ENDPOINT_SETTINGS_KEY = "/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings" +DEVICE_SETTINGS_KEY = "/device[{:s}]/settings" +RE_CONFIG_RULE_IF_SUBIF = re.compile(r"^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$") +MEC_FIELDS = [ + "device", "applicationServer", "qosProfile", "sessionId", "duration", + "startedAt", "expiresAt", "qosStatus" ] -def __init__(self) -> None: - super().__init__() - self.qos_profile_client = QoSProfileClient() - self.client = ContextClient() +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))) -def grpc_message_to_qos_table_data(message: QoSProfile) -> dict: +def grpc_message_to_qos_table_data(message : QoSProfile) -> dict: return { - 'qos_profile_id' : message.qos_profile_id.qos_profile_id.uuid, - 'name' : message.name, - 'description' : message.description, - 'status' : message.status, - 'targetMinUpstreamRate' : grpc_message_to_json(message.targetMinUpstreamRate), - 'maxUpstreamRate' : grpc_message_to_json(message.maxUpstreamRate), - 'maxUpstreamBurstRate' : grpc_message_to_json(message.maxUpstreamBurstRate), - 'targetMinDownstreamRate' : grpc_message_to_json(message.targetMinDownstreamRate), - 'maxDownstreamRate' : grpc_message_to_json(message.maxDownstreamRate), - 'maxDownstreamBurstRate' : grpc_message_to_json(message.maxDownstreamBurstRate), - 'minDuration' : grpc_message_to_json(message.minDuration), - 'maxDuration' : grpc_message_to_json(message.maxDuration), - 'priority' : message.priority, - 'packetDelayBudget' : grpc_message_to_json(message.packetDelayBudget), - 'jitter' : grpc_message_to_json(message.jitter), - 'packetErrorLossRate' : message.packetErrorLossRate, + "qos_profile_id" : message.qos_profile_id.qos_profile_id.uuid, + "name" : message.name, + "description" : message.description, + "status" : message.status, + "targetMinUpstreamRate" : grpc_message_to_json(message.targetMinUpstreamRate), + "maxUpstreamRate" : grpc_message_to_json(message.maxUpstreamRate), + "maxUpstreamBurstRate" : grpc_message_to_json(message.maxUpstreamBurstRate), + "targetMinDownstreamRate" : grpc_message_to_json(message.targetMinDownstreamRate), + "maxDownstreamRate" : grpc_message_to_json(message.maxDownstreamRate), + "maxDownstreamBurstRate" : grpc_message_to_json(message.maxDownstreamBurstRate), + "minDuration" : grpc_message_to_json(message.minDuration), + "maxDuration" : grpc_message_to_json(message.maxDuration), + "priority" : message.priority, + "packetDelayBudget" : grpc_message_to_json(message.packetDelayBudget), + "jitter" : grpc_message_to_json(message.jitter), + "packetErrorLossRate" : message.packetErrorLossRate, } -def create_qos_profile_from_json(qos_profile_data: dict) -> QoSProfile: - def create_QoSProfileValueUnitPair(data) -> QoSProfileValueUnitPair: - return QoSProfileValueUnitPair(value=data['value'], unit=data['unit']) +def create_value_unit(data) -> QoSProfileValueUnitPair: + return QoSProfileValueUnitPair(value=data["value"], unit=data["unit"]) + +def create_qos_profile_from_json(qos_profile_data : dict) -> QoSProfile: qos_profile = QoSProfile() - qos_profile.qos_profile_id.CopyFrom(QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_data['qos_profile_id']))) - qos_profile.name = qos_profile_data['name'] - qos_profile.description = qos_profile_data['description'] - qos_profile.status = qos_profile_data['status'] - qos_profile.targetMinUpstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['targetMinUpstreamRate'])) - qos_profile.maxUpstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxUpstreamRate'])) - qos_profile.maxUpstreamBurstRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxUpstreamBurstRate'])) - qos_profile.targetMinDownstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['targetMinDownstreamRate'])) - qos_profile.maxDownstreamRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDownstreamRate'])) - qos_profile.maxDownstreamBurstRate.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDownstreamBurstRate'])) - qos_profile.minDuration.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['minDuration'])) - qos_profile.maxDuration.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['maxDuration'])) - qos_profile.priority = qos_profile_data['priority'] - qos_profile.packetDelayBudget.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['packetDelayBudget'])) - qos_profile.jitter.CopyFrom(create_QoSProfileValueUnitPair(qos_profile_data['jitter'])) - qos_profile.packetErrorLossRate = qos_profile_data['packetErrorLossRate'] + qos_profile.qos_profile_id.CopyFrom(QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_data["qos_profile_id"]))) + qos_profile.name = qos_profile_data["name"] + qos_profile.description = qos_profile_data["description"] + qos_profile.status = qos_profile_data["status"] + qos_profile.targetMinUpstreamRate.CopyFrom(create_value_unit(qos_profile_data["targetMinUpstreamRate"])) + qos_profile.maxUpstreamRate.CopyFrom(create_value_unit(qos_profile_data["maxUpstreamRate"])) + qos_profile.maxUpstreamBurstRate.CopyFrom(create_value_unit(qos_profile_data["maxUpstreamBurstRate"])) + qos_profile.targetMinDownstreamRate.CopyFrom(create_value_unit(qos_profile_data["targetMinDownstreamRate"])) + qos_profile.maxDownstreamRate.CopyFrom(create_value_unit(qos_profile_data["maxDownstreamRate"])) + qos_profile.maxDownstreamBurstRate.CopyFrom(create_value_unit(qos_profile_data["maxDownstreamBurstRate"])) + qos_profile.minDuration.CopyFrom(create_value_unit(qos_profile_data["minDuration"])) + qos_profile.maxDuration.CopyFrom(create_value_unit(qos_profile_data["maxDuration"])) + qos_profile.priority = qos_profile_data["priority"] + qos_profile.packetDelayBudget.CopyFrom(create_value_unit(qos_profile_data["packetDelayBudget"])) + qos_profile.jitter.CopyFrom(create_value_unit(qos_profile_data["jitter"])) + qos_profile.packetErrorLossRate = qos_profile_data["packetErrorLossRate"] return qos_profile -def ip_withoutsubnet(ip_withsubnet,neededip): +def ip_withoutsubnet(ip_withsubnet, target_ip_address): network = IPNetwork(ip_withsubnet) - return IPAddress(neededip) in network - + return IPAddress(target_ip_address) in network + +def map_ip_addresses_to_endpoint_ids( + context_client : ContextClient, a_ip : str, z_ip : str +) -> Tuple[EndPointId, EndPointId]: + a_ep_id = None + z_ep_id = None + + devices = context_client.ListDevices(Empty()).devices + for device in devices: + endpoint_mappings = dict() + for endpoint in device.device_endpoints: + endpoint_id = endpoint.endpoint_id + endpoint_uuid = endpoint_id.endpoint_uuid.uuid + endpoint_name = endpoint.name + endpoint_mappings[endpoint_uuid] = endpoint_id + endpoint_mappings[endpoint_name] = endpoint_id + + for config_rule in device.device_config.config_rules: + if config_rule.WhichOneof("config_rule") != "custom": continue + match_subif = RE_CONFIG_RULE_IF_SUBIF.match(config_rule.custom.resource_key) + if not match_subif: continue + + short_port_name = match_subif.groups()[0] + endpoint_id = endpoint_mappings[short_port_name] + + address_ip = json.loads(config_rule.custom.resource_value).get("address_ip") + if ip_withoutsubnet(a_ip, address_ip): a_ep_id = endpoint_id + if ip_withoutsubnet(z_ip, address_ip): z_ep_id = endpoint_id + + return a_ep_id, z_ep_id + +def QOD_2_service( + context_client : ContextClient, qos_profile_client : QoSProfileClient, + qod_info : Dict +) -> Service: + + if "session_id" not in qod_info: + session_id = str(uuid4()) + qod_info["session_id"] = session_id -def QOD_2_service(client,qod_info: dict,qos_profile_id: int,duration: int) -> Service: - qos_profile_client = QoSProfileClient() service = Service() - service_config_rules = service.service_config.config_rules - - request_cr_key = '/request' - request_cr_value = {k: qod_info[k] for k in MEC_CONSIDERED_FIELDS if k in qod_info} - config_rule = ConfigRule() - config_rule.action = ConfigActionEnum.CONFIGACTION_SET - config_rule_custom = ConfigRule_Custom() - config_rule_custom.resource_key = request_cr_key - config_rule_custom.resource_value = json.dumps(request_cr_value) - config_rule.custom.CopyFrom(config_rule_custom) - service_config_rules.append(config_rule) + service.service_id.service_uuid.uuid = session_id + service.service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME + service.name = session_id + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED if 'device' in qod_info and 'applicationServer' in qod_info: a_ip = qod_info['device'].get('ipv4Address') z_ip = qod_info['applicationServer'].get('ipv4Address') - - LOGGER.debug('a_ip = {:s}'.format(str(a_ip))) - LOGGER.debug('z_ip = {:s}'.format(str(z_ip))) - if a_ip and z_ip: - devices = client.ListDevices(Empty()).devices - #LOGGER.info('devices = {:s}'.format(str(devices))) - - ip_interface_name_dict = {} - - for device in devices: - #LOGGER.info('device..uuid = {:s}'.format(str(device.device_id.device_uuid.uuid))) - #LOGGER.info('device..name = {:s}'.format(str(device.name))) - device_endpoint_uuids = {ep.name: ep.endpoint_id.endpoint_uuid.uuid for ep in device.device_endpoints} - #LOGGER.info('device_endpoint_uuids = {:s}'.format(str(device_endpoint_uuids))) - - for cr in device.device_config.config_rules: - if cr.WhichOneof('config_rule') != 'custom': - continue - #LOGGER.info('cr = {:s}'.format(str(cr))) - match_subif = RE_CONFIG_RULE_IF_SUBIF.match(cr.custom.resource_key) - if not match_subif: - continue - address_ip =json.loads(cr.custom.resource_value).get('address_ip') - LOGGER.debug('cr..address_ip = {:s}'.format(str(address_ip))) - short_port_name = match_subif.groups()[0] - LOGGER.debug('short_port_name = {:s}'.format(str(short_port_name))) - - ip_interface_name_dict[address_ip] = short_port_name - - if not (ip_withoutsubnet(a_ip, address_ip) or ip_withoutsubnet(z_ip, address_ip)): - continue - ep_id = EndPointId() - ep_id.endpoint_uuid.uuid = device_endpoint_uuids.get(short_port_name , '') - ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid - service.service_endpoint_ids.append(ep_id) - LOGGER.debug(f"the ip address{ep_id}") - - #LOGGER.info('ip_interface_name_dict = {:s}'.format(str(ip_interface_name_dict))) - - settings_cr_key = '/settings' - settings_cr_value = {} - update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) - service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED - service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM - qod_info["sessionID"]=str(uuid4()) - qod_info["context"]='admin' - service.service_id.service_uuid.uuid = qod_info['sessionID'] - service.service_id.context_id.context_uuid.uuid = qod_info["context"] - service.name = qod_info.get('qos_profile_id', qos_profile_id) - current_time = time.time() - duration_days = duration # days as i saw it in the proto files - id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) - request = QoDConstraintsRequest(qos_profile_id=id,start_timestamp=current_time,duration=duration_days) #defined attributes in proto file for the QoDconstraint rquest message - qos_profiles_constraint = qos_profile_client.GetConstraintListFromQoSProfile(request) - LOGGER.info(f'current time contains{current_time}') - LOGGER.info(f'current duration time contains{duration_days}') - LOGGER.info(f'id : {id}') - for cs in qos_profiles_constraint: - if cs.WhichOneof('constraint') == 'schedule' and cs.WhichOneof('constraint') == 'qos_profile': #the method of which one of - cs.schedule.start_timestamp = current_time - cs.schedule.duration_days = duration_days - cs.qos_profile.qos_profile_id=id - cs.qos_profile.qos_profile_name.CopyFrom(qos_profile_client.name) #i copied this from the qosprofile - LOGGER.info(f'the cs : {cs}') - service.service_constraints.append(cs) - - return service + a_ep_id, z_ep_id = map_ip_addresses_to_endpoint_ids(context_client, a_ip, z_ip) + if a_ep_id is not None: service.service_endpoint_ids.append(a_ep_id) + if z_ep_id is not None: service.service_endpoint_ids.append(z_ep_id) + service_config_rules = service.service_config.config_rules + update_config_rule_custom(service_config_rules, '/settings', {}) + update_config_rule_custom(service_config_rules, '/request', { + k : (qod_info[k], True) for k in MEC_FIELDS if k in qod_info + }) + + qos_profile_id = qod_info.get('qos_profile_id') + qos_profile_id = QoSProfileId(qos_profile_id=Uuid(uuid=qos_profile_id)) + current_time = time.time() + duration_days = qod_info.get('duration') + request = QoDConstraintsRequest( + qos_profile_id=qos_profile_id, start_timestamp=current_time, duration=duration_days + ) + qos_profile_constraints = qos_profile_client.GetConstraintsFromQoSProfile(request) + LOGGER.warning('qos_profile_constraints = {:s}'.format(grpc_message_to_json_string(qos_profile_constraints))) + copy_constraints(qos_profile_constraints.constraints, service.service_constraints) + LOGGER.warning('service.service_constraints = {:s}'.format(grpc_message_to_json_string(service.service_constraints))) + return service -def service_2_qod(service: Service) -> dict: +def service_2_qod(service : Service) -> Dict: response = {} for config_rule in service.service_config.config_rules: + if config_rule.WhichOneof("config_rule") != "custom": continue + if config_rule.custom.resource_key != '/request': continue resource_value_json = json.loads(config_rule.custom.resource_value) - LOGGER.info(f"the resource value contains:{resource_value_json}") - if config_rule.custom.resource_key != '/request': - continue - if 'device' in resource_value_json and 'ipv4Address' in resource_value_json['device']: - response['device'] = { - 'ipv4Address': resource_value_json['device']['ipv4Address'] - } - - if 'applicationServer' in resource_value_json and 'ipv4Address' in resource_value_json['applicationServer']: - response['applicationServer'] = { - 'ipv4Address': resource_value_json['applicationServer']['ipv4Address'] - } - - if service.name: - response['qos_profile_id'] = service.name - - if service.service_id: - response['sessionId'] = service.service_id.service_uuid.uuid - if service.service_constraints: - for constraint in service.service_constraints: - if constraint.WhichOneof('constraint') == 'schedule': - response['duration'] = float(constraint.schedule.duration_days* (86400)) - LOGGER.info(f'the duration in seconds: {response["duration"]}') - response['startedAt'] = int(constraint.schedule.start_timestamp) - response['expiresAt'] = response['startedAt'] + response['duration'] + if 'device' in resource_value_json and 'ipv4Address' in resource_value_json['device']: + response['device'] = {'ipv4Address': resource_value_json['device']['ipv4Address']} - return response + if 'applicationServer' in resource_value_json and 'ipv4Address' in resource_value_json['applicationServer']: + response['applicationServer'] = {'ipv4Address': resource_value_json['applicationServer']['ipv4Address']} + if service.service_id: + response['session_id'] = service.service_id.service_uuid.uuid -def format_grpc_to_json(grpc_reply): - return jsonify(grpc_message_to_json(grpc_reply)) + for constraint in service.service_constraints: + if constraint.WhichOneof('constraint') == 'schedule': + response['duration' ] = float(constraint.schedule.duration_days) + response['startedAt'] = int(constraint.schedule.start_timestamp) + response['expiresAt'] = response['startedAt'] + response['duration'] -def grpc_context_id(context_uuid): - return ContextId(**json_context_id(context_uuid)) + if constraint.WhichOneof('constraint') == 'qos_profile': + response['qos_profile_id'] = constraint.qos_profile.qos_profile_id.qos_profile_id.uuid -def grpc_service_id(context_uuid, service_uuid): - return ServiceId(**json_service_id(service_uuid, context_id=json_context_id(context_uuid))) + return response diff --git a/src/nbi/service/camara_qod/__init__.py b/src/nbi/service/camara_qod/__init__.py index 9ec02d8fe..293088bfc 100644 --- a/src/nbi/service/camara_qod/__init__.py +++ b/src/nbi/service/camara_qod/__init__.py @@ -21,12 +21,12 @@ def register_camara_qod(nbi_app : NbiApplication): nbi_app.add_rest_api_resource( QodInfo, URL_PREFIX + '/sessions', - endpoint='camara.qod.session_info' + endpoint='camara.qod.session_list' ) nbi_app.add_rest_api_resource( QodInfoID, - URL_PREFIX + '/sessions/', - endpoint='camara.qod.info_session_id' + URL_PREFIX + '/sessions/', + endpoint='camara.qod.session_detail' ) nbi_app.add_rest_api_resource( ProfileList, diff --git a/src/nbi/tests/MockService_Dependencies.py b/src/nbi/tests/MockService_Dependencies.py deleted file mode 100644 index 69a8a0b24..000000000 --- a/src/nbi/tests/MockService_Dependencies.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2022-2024 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, signal, sys, threading -from common.proto.context_pb2_grpc import add_ContextServiceServicer_to_server -from common.proto.service_pb2_grpc import add_ServiceServiceServicer_to_server -from common.proto.slice_pb2_grpc import add_SliceServiceServicer_to_server -from common.tests.MockServicerImpl_Context import MockServicerImpl_Context -from common.tests.MockServicerImpl_Service import MockServicerImpl_Service -from common.tests.MockServicerImpl_Slice import MockServicerImpl_Slice -from common.tools.service.GenericGrpcService import GenericGrpcService -from .Constants import LOCAL_HOST, MOCKSERVICE_PORT - - -logging.basicConfig( - level=logging.DEBUG, - format='[%(asctime)s] %(levelname)s:%(name)s:%(message)s', -) -LOGGER = logging.getLogger(__name__) - -class MockService_Dependencies(GenericGrpcService): - # Mock Service implementing Context, Service and Slice to simplify unitary tests of NBI - - def __init__(self) -> None: - super().__init__( - MOCKSERVICE_PORT, LOCAL_HOST, - enable_health_servicer=False, - cls_name='MockService' - ) - - # pylint: disable=attribute-defined-outside-init - def install_servicers(self): - self.context_servicer = MockServicerImpl_Context() - add_ContextServiceServicer_to_server(self.context_servicer, self.server) - - self.service_servicer = MockServicerImpl_Service() - add_ServiceServiceServicer_to_server(self.service_servicer, self.server) - - self.slice_servicer = MockServicerImpl_Slice() - add_SliceServiceServicer_to_server(self.slice_servicer, self.server) - -TERMINATE = threading.Event() - -def signal_handler(signal, frame): # pylint: disable=redefined-outer-name,unused-argument - LOGGER.warning('Terminate signal received') - TERMINATE.set() - -def main(): - LOGGER.info('Starting...') - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - grpc_service = MockService_Dependencies() - grpc_service.start() - - # Wait for Ctrl+C or termination signal - while not TERMINATE.wait(timeout=1.0): pass - - LOGGER.info('Terminating...') - grpc_service.stop() - - LOGGER.info('Bye') - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/nbi/tests/PrepareTestScenario.py b/src/nbi/tests/PrepareTestScenario.py index 1510dd298..57f1476a8 100644 --- a/src/nbi/tests/PrepareTestScenario.py +++ b/src/nbi/tests/PrepareTestScenario.py @@ -36,63 +36,25 @@ from .OSM_Constants import WIM_MAPPING from .MockWebServer import MockWebServer -os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) -os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_PORT_HTTP)] = str(NBI_SERVICE_PORT) -os.environ[get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST )] = str('mock_tfs_nbi_dependencies') -os.environ[get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(MOCKSERVICE_PORT) -os.environ[get_env_var_name(ServiceNameEnum.DEVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str('mock_tfs_nbi_dependencies') -os.environ[get_env_var_name(ServiceNameEnum.DEVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(MOCKSERVICE_PORT) -os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_HOST )] = str('mock_tfs_nbi_dependencies') -os.environ[get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(MOCKSERVICE_PORT) -os.environ[get_env_var_name(ServiceNameEnum.SLICE, ENVVAR_SUFIX_SERVICE_HOST )] = str('mock_tfs_nbi_dependencies') -os.environ[get_env_var_name(ServiceNameEnum.SLICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(MOCKSERVICE_PORT) - - -## MockService_Dependencies executed as a standalone container during -# tests to prevent apparent dead locks and issues. -#@pytest.fixture(scope='session') -#def mock_service(): -# # NOTE: Starting MockServer in a separate process to prevent -# # issues with eventlet monkey-patched libraries. -# -# cmd = ['python', '-m', 'nbi.tests.MockService_Dependencies'] -# custom_env = os.environ.copy() -# mock_service_process = subprocess.Popen( -# cmd, -# env=custom_env, -# stdout=subprocess.PIPE, -# stderr=subprocess.STDOUT, -# stdin=subprocess.DEVNULL, -# text=True, -# bufsize=1 -# ) -# -# mock_service_logger = logging.getLogger('MockService_Dependencies') -# mock_service_logger.info('Started') -# -# def stream_stdout(): -# for line in iter(mock_service_process.stdout.readline, ''): -# mock_service_logger.info(line.strip()) -# -# stream_stdout_thread = threading.Thread(target=stream_stdout, daemon=True) -# stream_stdout_thread.start() -# -# yield True -# -# # Check if process is still running -# if mock_service_process.poll() is None: -# mock_service_process.terminate() # Try to terminate gracefully -# time.sleep(2) # Give it time to exit -# if mock_service_process.poll() is None: -# mock_service_process.kill() # Force kill if still running -# -# mock_service_logger.info('Terminated') -# stream_stdout_thread.join() +os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) +os.environ[get_env_var_name(ServiceNameEnum.NBI, ENVVAR_SUFIX_SERVICE_PORT_HTTP)] = str(NBI_SERVICE_PORT) + +MOCK_SERVICES = [ + ServiceNameEnum.CONTEXT, + ServiceNameEnum.DEVICE, + ServiceNameEnum.QOSPROFILE, + ServiceNameEnum.SERVICE, + ServiceNameEnum.SLICE, +] +for mock_service in MOCK_SERVICES: + mock_service_host_env_var = get_env_var_name(mock_service, ENVVAR_SUFIX_SERVICE_HOST) + os.environ[mock_service_host_env_var] = str('mock_tfs_nbi_dependencies') + mock_service_port_env_var = get_env_var_name(mock_service, ENVVAR_SUFIX_SERVICE_PORT_GRPC) + os.environ[mock_service_port_env_var] = str(MOCKSERVICE_PORT) + @pytest.fixture(scope='session') -def nbi_application( -# mock_service # pylint: disable=redefined-outer-name, unused-argument -) -> NbiApplication: +def nbi_application() -> NbiApplication: mock_web_server = MockWebServer() mock_web_server.start() time.sleep(1) # bring time for the server to start @@ -116,33 +78,25 @@ def osm_wim( return MockOSM(wim_url, WIM_MAPPING, USERNAME, PASSWORD) @pytest.fixture(scope='session') -def context_client( -# mock_service # pylint: disable=redefined-outer-name, unused-argument -) -> ContextClient: +def context_client() -> ContextClient: _client = ContextClient() yield _client _client.close() @pytest.fixture(scope='session') -def device_client( -# mock_service # pylint: disable=redefined-outer-name, unused-argument -) -> DeviceClient: +def device_client() -> DeviceClient: _client = DeviceClient() yield _client _client.close() @pytest.fixture(scope='session') -def service_client( -# mock_service # pylint: disable=redefined-outer-name, unused-argument -) -> ServiceClient: +def service_client() -> ServiceClient: _client = ServiceClient() yield _client _client.close() @pytest.fixture(scope='session') -def slice_client( -# mock_service # pylint: disable=redefined-outer-name, unused-argument -) -> SliceClient: +def slice_client() -> SliceClient: _client = SliceClient() yield _client _client.close() diff --git a/src/nbi/tests/test_camara_qod.py b/src/nbi/tests/test_camara_qod.py index 2230fa3a4..de78b1bbd 100644 --- a/src/nbi/tests/test_camara_qod.py +++ b/src/nbi/tests/test_camara_qod.py @@ -20,24 +20,55 @@ eventlet.monkey_patch() #pylint: disable=wrong-import-position import deepdiff, logging, pytest +from decimal import ROUND_HALF_UP, Decimal from typing import Dict +from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME +from common.proto.context_pb2 import ContextId +from common.tools.descriptor.Loader import ( + DescriptorLoader, check_descriptor_load_results, validate_empty_scenario +) +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient from nbi.service.NbiApplication import NbiApplication from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - nbi_application, do_rest_delete_request, do_rest_get_request, - do_rest_post_request, do_rest_put_request, + nbi_application, context_client, + do_rest_delete_request, do_rest_get_request, do_rest_post_request, do_rest_put_request, ) LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) +DESCRIPTOR_FILE = 'nbi/tests/data/tfs_api_dummy.json' + +JSON_ADMIN_CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_NAME) +ADMIN_CONTEXT_ID = ContextId(**JSON_ADMIN_CONTEXT_ID) + @pytest.fixture(scope='session') def storage() -> Dict: yield dict() +# ----- Prepare Environment -------------------------------------------------------------------------------------------- + +def test_prepare_environment(context_client : ContextClient) -> None: # pylint: disable=redefined-outer-name + validate_empty_scenario(context_client) + descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client) + results = descriptor_loader.process() + check_descriptor_load_results(results, descriptor_loader) + descriptor_loader.validate() + + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.topology_ids) == 1 + assert len(response.service_ids ) == 3 + assert len(response.slice_ids ) == 1 + + +# ----- Run tests ------------------------------------------------------------------------------------------------------ + def test_create_profile( nbi_application : NbiApplication, # pylint: disable=redefined-outer-name storage : Dict # pylint: disable=redefined-outer-name, unused-argument @@ -61,7 +92,7 @@ def test_create_profile( } post_response = do_rest_post_request( '/camara/qod/v0/profiles', body=qos_profile_data, - expected_status_codes={200, 201, 202} + expected_status_codes={201} ) assert 'qos_profile_id' in post_response qos_profile_data['qos_profile_id'] = post_response['qos_profile_id'] @@ -82,7 +113,7 @@ def test_get_profile_before_update( get_response = do_rest_get_request( '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={200} ) diff_data = deepdiff.DeepDiff(qos_profile, get_response) @@ -116,8 +147,8 @@ def test_update_profile( "packetErrorLossRate" : 1 } put_response = do_rest_put_request( - '/camara/qod/v0/profiles', body=qos_profile_update, - expected_status_codes={200, 201, 202} + '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), body=qos_profile_update, + expected_status_codes={202} ) diff_data = deepdiff.DeepDiff(qos_profile_update, put_response) @@ -136,7 +167,7 @@ def test_get_profile_after_update( get_response = do_rest_get_request( '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={200} ) diff_data = deepdiff.DeepDiff(qos_profile, get_response) @@ -147,25 +178,34 @@ def test_create_session( nbi_application : NbiApplication, # pylint: disable=redefined-outer-name storage : Dict # pylint: disable=redefined-outer-name, unused-argument ) -> None: + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + session_data = { - "device" : {"ipv4Address":"84.75.11.12/25"}, + "device" : {"ipv4Address": "84.75.11.12/25"}, "applicationServer": {"ipv4Address": "192.168.0.1/26"}, - "duration" : 10000000.00, - "qos_profile_id" : "f46d563f-9f1a-44c8-9649-5fde673236d6", + "duration" : float(10), # 10 days + "qos_profile_id" : qos_profile_id, } post_response = do_rest_post_request( '/camara/qod/v0/sessions', body=session_data, - expected_status_codes={200, 201, 202} + expected_status_codes={201} ) assert 'session_id' in post_response session_data['session_id'] = post_response['session_id'] + del post_response['duration'] + del session_data['duration'] + del post_response['startedAt'] + del post_response['expiresAt'] + diff_data = deepdiff.DeepDiff(session_data, post_response) LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) assert len(diff_data) == 0 - storage['session'] = session_data + storage['session'] = post_response def test_get_session_before_update( nbi_application : NbiApplication, # pylint: disable=redefined-outer-name @@ -177,9 +217,13 @@ def test_get_session_before_update( get_response = do_rest_get_request( '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={200} ) + del get_response['duration'] + del get_response['startedAt'] + del get_response['expiresAt'] + diff_data = deepdiff.DeepDiff(session, get_response) LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) assert len(diff_data) == 0 @@ -192,18 +236,27 @@ def test_update_session( assert 'session_id' in session session_id = session['session_id'] + qos_profile = storage['qos_profile'] + assert 'qos_profile_id' in qos_profile + qos_profile_id = qos_profile['qos_profile_id'] + session_update = { "session_id" : session_id, - "device" : {"ipv4Address":"84.75.11.12/25"}, + "device" : {"ipv4Address": "84.75.11.12/25"}, "applicationServer": {"ipv4Address": "192.168.0.1/26"}, - "duration" : 2000000000.00, - "qos_profile_id" : "f46d563f-9f1a-44c8-9649-5fde673236d6", + "duration" : float(20), # 20 days + "qos_profile_id" : qos_profile_id, } put_response = do_rest_put_request( - '/camara/qod/v0/sessions', body=session_update, - expected_status_codes={200, 201, 202} + '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), body=session_update, + expected_status_codes={202} ) + del put_response['duration'] + del session_update['duration'] + del put_response['startedAt'] + del put_response['expiresAt'] + diff_data = deepdiff.DeepDiff(session_update, put_response) LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) assert len(diff_data) == 0 @@ -220,9 +273,13 @@ def test_get_session_after_update( get_response = do_rest_get_request( '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={200} ) + del get_response['duration'] + del get_response['startedAt'] + del get_response['expiresAt'] + diff_data = deepdiff.DeepDiff(session, get_response) LOGGER.error('Differences:\n{:s}'.format(str(diff_data.pretty()))) assert len(diff_data) == 0 @@ -236,7 +293,7 @@ def test_delete_session( session_id = session['session_id'] do_rest_delete_request( '/camara/qod/v0/sessions/{:s}'.format(str(session_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={204} ) storage.pop('session') @@ -249,6 +306,21 @@ def test_delete_profile( qos_profile_id = qos_profile['qos_profile_id'] do_rest_delete_request( '/camara/qod/v0/profiles/{:s}'.format(str(qos_profile_id)), - expected_status_codes={200, 201, 202} + expected_status_codes={204} ) storage.pop('qos_profile') + +# ----- Cleanup Environment -------------------------------------------------------------------------------------------- + +def test_cleanup_environment(context_client : ContextClient) -> None: # pylint: disable=redefined-outer-name + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.topology_ids) == 1 + assert len(response.service_ids ) == 3 + assert len(response.slice_ids ) == 1 + + # Load descriptors and validate the base scenario + descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client) + descriptor_loader.validate() + descriptor_loader.unload() + validate_empty_scenario(context_client) diff --git a/src/nbi/tests/test_core.py b/src/nbi/tests/test_core.py index 39db882c0..8d49c5bc6 100644 --- a/src/nbi/tests/test_core.py +++ b/src/nbi/tests/test_core.py @@ -25,7 +25,6 @@ from .Constants import NBI_SERVICE_BASE_URL from .HeartbeatClientNamespace import HeartbeatClientNamespace from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, do_rest_get_request ) diff --git a/src/nbi/tests/test_etsi_bwm.py b/src/nbi/tests/test_etsi_bwm.py index 29666ffad..54acc3265 100644 --- a/src/nbi/tests/test_etsi_bwm.py +++ b/src/nbi/tests/test_etsi_bwm.py @@ -29,7 +29,6 @@ from context.client.ContextClient import ContextClient from nbi.service.NbiApplication import NbiApplication from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, context_client, do_rest_delete_request, do_rest_get_request, do_rest_patch_request, do_rest_post_request, do_rest_put_request ) diff --git a/src/nbi/tests/test_ietf_l2vpn.py b/src/nbi/tests/test_ietf_l2vpn.py index f620040e1..048ef56fa 100644 --- a/src/nbi/tests/test_ietf_l2vpn.py +++ b/src/nbi/tests/test_ietf_l2vpn.py @@ -28,7 +28,6 @@ from tests.tools.mock_osm.MockOSM import MockOSM from .OSM_Constants import SERVICE_CONNECTION_POINTS_1, SERVICE_CONNECTION_POINTS_2, SERVICE_TYPE from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, osm_wim, context_client ) diff --git a/src/nbi/tests/test_ietf_l3vpn.py b/src/nbi/tests/test_ietf_l3vpn.py index c3176c25a..7cdb5fcf1 100644 --- a/src/nbi/tests/test_ietf_l3vpn.py +++ b/src/nbi/tests/test_ietf_l3vpn.py @@ -30,7 +30,6 @@ from context.client.ContextClient import ContextClient from nbi.service.NbiApplication import NbiApplication from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, context_client, do_rest_delete_request, do_rest_get_request, do_rest_post_request ) diff --git a/src/nbi/tests/test_ietf_network.py b/src/nbi/tests/test_ietf_network.py index ceb61aac3..13e2392a4 100644 --- a/src/nbi/tests/test_ietf_network.py +++ b/src/nbi/tests/test_ietf_network.py @@ -34,7 +34,6 @@ os.environ['IETF_NETWORK_RENDERER'] = 'PYANGBIND' from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, context_client, do_rest_get_request ) diff --git a/src/nbi/tests/test_tfs_api.py b/src/nbi/tests/test_tfs_api.py index ed5630a9b..e3278cd0d 100644 --- a/src/nbi/tests/test_tfs_api.py +++ b/src/nbi/tests/test_tfs_api.py @@ -39,7 +39,6 @@ from context.client.ContextClient import ContextClient from nbi.service.NbiApplication import NbiApplication from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! - #mock_service, nbi_application, context_client, do_rest_get_request ) -- GitLab From 69b2305152bfdc50a646fc31633afb7a0a6546bf Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 15:32:54 +0000 Subject: [PATCH 18/19] QoS Profile component: - Corrected unitary test --- src/qos_profile/tests/test_crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qos_profile/tests/test_crud.py b/src/qos_profile/tests/test_crud.py index 04125401c..c5851509b 100644 --- a/src/qos_profile/tests/test_crud.py +++ b/src/qos_profile/tests/test_crud.py @@ -92,9 +92,9 @@ def test_get_qos_profile(qos_profile_client: QoSProfileClient): def test_get_qos_profiles(qos_profile_client: QoSProfileClient): qos_profile = create_qos_profile_from_json(qos_profile_data) - qos_profiles_got = list(qos_profile_client.GetQoSProfiles(Empty())) - the_qos_profile = [q for q in qos_profiles_got if q.qos_profile_id == qos_profile.qos_profile_id] + qos_profiles_got = qos_profile_client.GetQoSProfiles(Empty()) LOGGER.info('qos_profile_data = {:s}'.format(grpc_message_to_json_string(qos_profiles_got))) + the_qos_profile = [q for q in qos_profiles_got.qos_profiles if q.qos_profile_id == qos_profile.qos_profile_id] assert len(the_qos_profile) == 1 assert qos_profile == the_qos_profile[0] -- GitLab From 5b6394cf8212d30e50fac3b6c87ce9528f7532ef Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 15 May 2025 15:38:49 +0000 Subject: [PATCH 19/19] CAMARA OFC25 - End-to-end tests: - Deactivated in CI/CD pipeline as mock server used lacks support for retrieving topology, thus it fails --- src/tests/.gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml index 787b25ee1..9c441d746 100644 --- a/src/tests/.gitlab-ci.yml +++ b/src/tests/.gitlab-ci.yml @@ -21,8 +21,8 @@ include: #- local: '/src/tests/ofc23/.gitlab-ci.yml' - local: '/src/tests/ofc24/.gitlab-ci.yml' - local: '/src/tests/eucnc24/.gitlab-ci.yml' - - local: '/src/tests/ofc25-camara-agg-net-controller/.gitlab-ci.yml' - - local: '/src/tests/ofc25-camara-e2e-controller/.gitlab-ci.yml' + #- local: '/src/tests/ofc25-camara-agg-net-controller/.gitlab-ci.yml' + #- local: '/src/tests/ofc25-camara-e2e-controller/.gitlab-ci.yml' #- local: '/src/tests/ofc25/.gitlab-ci.yml' - local: '/src/tests/tools/mock_tfs_nbi_dependencies/.gitlab-ci.yml' -- GitLab