diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index 72cfde514341b0ef89b3b8eb91ab01b23a27c14b..bf1b427a0c658163ab10366e399cc35c1facb203 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/nginx_ingress_http.yaml b/manifests/nginx_ingress_http.yaml index 619d85f7a82af48c71464038bf14a833903d1a58..8f96f000e69881ec0c5a3cc42a11548feb2900e7 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 diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml index 801607880bbcd9a51bacbec396f797dda7132d81..ebc218319b484f424e09f862b7d94dbb8b1fc221 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 8d2e733d462ae743c7187eb9b6a58d7da14033a7..5da39cb854263569878df0c3fdec06f64278ce55 --- 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 ec1c054858b5f97dcdff36e2ccd4c8039942b51e..78e1fdc8d1564690384437ad2ebc953807a38517 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 fb735f8a775e8cce1bc696ed4f148b2ab0ec9dcc..c1eb08da267ebabf4c594ad1b2ec7b7bafef2c7f 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 0000000000000000000000000000000000000000..a72de0323368a9131ee09c88a9a41d724226f85f --- /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 0000000000000000000000000000000000000000..043e5d92adc02bb60d5c50515b55a5b4b6f76576 --- /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 0000000000000000000000000000000000000000..9b19a1f9e351123db2207cd27c93bf3f6d1fbe13 --- /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 0000000000000000000000000000000000000000..7e2eebe79afee6a2a2463afc8d0ea173a7e90a99 --- /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 0000000000000000000000000000000000000000..24726851203cdf77caf7aa2c20c8fd2e68fb8473 --- /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