diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1bcaa500fd204cef1bdcde0db35306113f9cacd1 --- /dev/null +++ b/manifests/qos_profileservice.yaml @@ -0,0 +1,72 @@ +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qos-profileservice +spec: + selector: + matchLabels: + app: qos-profileservice + #replicas: 1 + template: + metadata: + labels: + app: qos-profileservice + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: labs.etsi.org:5050/tfs/controller/qos_profile:latest + imagePullPolicy: Always + ports: + - containerPort: 30060 + - containerPort: 9192 + env: + - name: LOG_LEVEL + value: "INFO" + readinessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30060"] + livenessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30060"] + resources: + requests: + cpu: 250m + memory: 128Mi + limits: + cpu: 1000m + memory: 1024Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: qos-profileservice + labels: + app: qos-profileservice +spec: + type: ClusterIP + selector: + app: qos-profileservice + ports: + - name: grpc + protocol: TCP + port: 30060 + targetPort: 30060 + - name: metrics + protocol: TCP + port: 9192 + targetPort: 9192 diff --git a/src/context/service/ContextServiceServicerImpl.py b/src/context/service/ContextServiceServicerImpl.py index ee4bc4de6a80ff07e5f49a80377721ccdfb3ba72..41454800088c39e67711cca2f00e993997f09971 100644 --- a/src/context/service/ContextServiceServicerImpl.py +++ b/src/context/service/ContextServiceServicerImpl.py @@ -264,11 +264,11 @@ class ContextServiceServicerImpl(ContextServiceServicer, ContextPolicyServiceSer @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) def DeleteQoSProfile(self, request : QoSProfileId, context : grpc.ServicerContext) -> Empty: - return delete_qos_profile(self.db_engine, request) + return delete_qos_profile(self.db_engine, request.qos_profile_id.uuid) @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) def GetQoSProfile(self, request : QoSProfileId, context : grpc.ServicerContext) -> QoSProfile: - return get_qos_profile(self.db_engine, request) + return get_qos_profile(self.db_engine, request.qos_profile_id.uuid) @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) def GetQoSProfiles(self, request : Empty, context : grpc.ServicerContext) -> Iterator[QoSProfile]: diff --git a/src/context/service/database/QoSProfile.py b/src/context/service/database/QoSProfile.py index 8b50f64def2ebd8235e94fade8f651b55c3d125f..8ffe8e0ecb17acf71c3b08e01d5bc240722a65ac 100644 --- a/src/context/service/database/QoSProfile.py +++ b/src/context/service/database/QoSProfile.py @@ -19,7 +19,7 @@ from sqlalchemy.orm import Session, sessionmaker from sqlalchemy_cockroachdb import run_transaction from typing import List, Optional -from common.proto.context_pb2 import Empty, QoSProfileId, QoSProfileValueUnitPair, QoSProfile +from common.proto.context_pb2 import Empty, Uuid, QoSProfileId, QoSProfileValueUnitPair, QoSProfile from common.method_wrappers.ServiceExceptions import NotFoundException from common.tools.grpc.Tools import grpc_message_to_json from .models.QoSProfile import QoSProfileModel @@ -27,8 +27,8 @@ from .models.QoSProfile import QoSProfileModel LOGGER = logging.getLogger(__name__) def grpc_message_to_qos_table_data(message: QoSProfile) -> dict: - return [{ - 'qos_profile_id' : message.qos_profile_id.uuid, + return { + 'qos_profile_id' : message.qos_profile_id.qos_profile_id.uuid, 'name' : message.name, 'description' : message.description, 'status' : message.status, @@ -44,11 +44,11 @@ def grpc_message_to_qos_table_data(message: QoSProfile) -> dict: 'packetDelayBudget' : grpc_message_to_json(message.packetDelayBudget), 'jitter' : grpc_message_to_json(message.jitter), 'packetErrorLossRate' : message.packetErrorLossRate, - }] + } def qos_table_data_to_grpc_message(data: QoSProfileModel) -> QoSProfile: - QoSProfile( - qos_profile_id = QoSProfileId(uuid=data.qos_profile_id), + return QoSProfile( + qos_profile_id = QoSProfileId(qos_profile_id=Uuid(uuid=data.qos_profile_id)), name = data.name, description = data.description, status = data.status, @@ -69,10 +69,32 @@ def qos_table_data_to_grpc_message(data: QoSProfileModel) -> QoSProfile: def set_qos_profile(db_engine : Engine, request : QoSProfile) -> QoSProfile: qos_profile_data = grpc_message_to_qos_table_data(request) def callback(session : Session) -> bool: - stmt = insert(QoSProfileModel).values(qos_profile_data) - session.execute(stmt) - return get_qos_profile(db_engine, request.qos_profile_id.uuid) - return run_transaction(sessionmaker(bind=db_engine), callback) + stmt = insert(QoSProfileModel).values([qos_profile_data]) + stmt = stmt.on_conflict_do_update(index_elements=[QoSProfileModel.qos_profile_id], + set_=dict( + + name = stmt.excluded.name, + description = stmt.excluded.description, + status = stmt.excluded.status, + targetMinUpstreamRate = stmt.excluded.targetMinUpstreamRate, + maxUpstreamRate = stmt.excluded.maxUpstreamRate, + maxUpstreamBurstRate = stmt.excluded.maxUpstreamBurstRate, + targetMinDownstreamRate = stmt.excluded.targetMinDownstreamRate, + maxDownstreamRate = stmt.excluded.maxDownstreamRate, + maxDownstreamBurstRate = stmt.excluded.maxDownstreamBurstRate, + minDuration = stmt.excluded.minDuration, + maxDuration = stmt.excluded.maxDuration, + priority = stmt.excluded.priority, + packetDelayBudget = stmt.excluded.packetDelayBudget, + jitter = stmt.excluded.jitter, + packetErrorLossRate = stmt.excluded.packetErrorLossRate, + ) + ) + stmt = stmt.returning(QoSProfileModel) + qos_profile = session.execute(stmt).fetchall() + return qos_profile[0] + qos_profile_row = run_transaction(sessionmaker(bind=db_engine), callback) + return qos_table_data_to_grpc_message(qos_profile_row) def delete_qos_profile(db_engine : Engine, request : str) -> Empty: def callback(session : Session) -> bool: diff --git a/src/context/service/database/models/QoSProfile.py b/src/context/service/database/models/QoSProfile.py index 0ce7365aa6b8c862604c194a2c6e4d3db29a00f5..431d0f503ce7c5224bff108d570c953ff6f538c6 100644 --- a/src/context/service/database/models/QoSProfile.py +++ b/src/context/service/database/models/QoSProfile.py @@ -13,19 +13,13 @@ # limitations under the License. from sqlalchemy import Column, Integer, String, JSON -from sqlalchemy.dialects.postgresql import UUID -from typing import TypedDict from ._Base import _Base -class QoSProfileValueUnitPair(TypedDict): - value: int - unit: str - class QoSProfileModel(_Base): __tablename__ = 'qos_profile' - qos_profile_id = Column(UUID(as_uuid=False), primary_key=True) + qos_profile_id = Column(String, primary_key=True) name = Column(String, nullable=False) description = Column(String, nullable=False) status = Column(String, nullable=False) diff --git a/src/qos_profile/Config.py b/src/qos_profile/Config.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/qos_profile/Config.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/qos_profile/Dockerfile b/src/qos_profile/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a35c2e741c8db372421bc8525f50e77f47e28be3 --- /dev/null +++ b/src/qos_profile/Dockerfile @@ -0,0 +1,76 @@ +# 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 python:3.9-slim + +# Install dependencies +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install wget g++ git && \ + rm -rf /var/lib/apt/lists/* + +# Set Python to show logs as they occur +ENV PYTHONUNBUFFERED=0 + +# Download the gRPC health probe +RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ + wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /bin/grpc_health_probe + +# Get generic Python packages +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install --upgrade setuptools wheel +RUN python3 -m pip install --upgrade pip-tools + +# Get common Python packages +# Note: this step enables sharing the previous Docker build steps among all the Python components +WORKDIR /var/teraflow +COPY common_requirements.in common_requirements.in +RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in +RUN python3 -m pip install -r common_requirements.txt + +# Add common files into working directory +WORKDIR /var/teraflow/common +COPY src/common/. ./ +RUN rm -rf proto + +# Create proto sub-folder, copy .proto files, and generate Python code +RUN mkdir -p /var/teraflow/common/proto +WORKDIR /var/teraflow/common/proto +RUN touch __init__.py +COPY proto/*.proto ./ +RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto +RUN rm *.proto +RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \; + +# Create component sub-folders, get specific Python packages +RUN mkdir -p /var/teraflow/qos_profile +WORKDIR /var/teraflow/qos_profile +COPY src/service/requirements.in requirements.in +RUN pip-compile --quiet --output-file=requirements.txt requirements.in +RUN python3 -m pip install -r requirements.txt + +# Add component files into working directory +WORKDIR /var/teraflow +COPY src/context/__init__.py context/__init__.py +COPY src/context/client/. context/client/ +COPY src/device/__init__.py device/__init__.py +COPY src/device/client/. device/client/ +COPY src/pathcomp/frontend/__init__.py pathcomp/frontend/__init__.py +COPY src/pathcomp/frontend/client/. pathcomp/frontend/client/ +COPY src/e2e_orchestrator/__init__.py e2e_orchestrator/__init__.py +COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/ +COPY src/qos_profile/. qos_profile/ + +# Start the service +ENTRYPOINT ["python", "-m", "qos_profile.service"] diff --git a/src/qos_profile/__init__.py b/src/qos_profile/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/qos_profile/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/qos_profile/client/QoSProfileClient.py b/src/qos_profile/client/QoSProfileClient.py new file mode 100644 index 0000000000000000000000000000000000000000..7a857ec25ed103a3bcb1c15dab04f3af19868884 --- /dev/null +++ b/src/qos_profile/client/QoSProfileClient.py @@ -0,0 +1,82 @@ +# 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 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, QoSProfile +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 + +LOGGER = logging.getLogger(__name__) +MAX_RETRIES = 15 +DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0) +RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') + +class QoSProfileClient: + def __init__(self, host=None, port=None): + if not host: host = get_service_host(ServiceNameEnum.QOSPROFILE) + if not port: port = get_service_port_grpc(ServiceNameEnum.QOSPROFILE) + self.endpoint = '{:s}:{:s}'.format(str(host), str(port)) + LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint))) + self.channel = None + self.stub = None + self.connect() + LOGGER.debug('Channel created') + + def connect(self): + self.channel = grpc.insecure_channel(self.endpoint) + self.stub = QoSProfileServiceStub(self.channel) + + def close(self): + if self.channel is not None: self.channel.close() + self.channel = None + self.stub = None + + @RETRY_DECORATOR + def CreateQoSProfile(self, request: QoSProfile) -> QoSProfile: + LOGGER.debug('CreateQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.CreateQoSProfile(request) + LOGGER.debug('CreateQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) + return response + + @RETRY_DECORATOR + def UpdateQoSProfile(self, request: QoSProfile) -> QoSProfile: + LOGGER.debug('UpdateQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.UpdateQoSProfile(request) + LOGGER.debug('UpdateQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) + return response + + @RETRY_DECORATOR + def DeleteQoSProfile(self, request: QoSProfileId) -> Empty: + LOGGER.debug('DeleteQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.DeleteQoSProfile(request) + LOGGER.debug('DeleteQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) + return response + + @RETRY_DECORATOR + def GetQoSProfile(self, request: QoSProfileId) -> QoSProfile: + LOGGER.debug('GetQoSProfile request: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.GetQoSProfile(request) + LOGGER.debug('GetQoSProfile result: {:s}'.format(grpc_message_to_json_string(response))) + return response + + @RETRY_DECORATOR + def GetQoSProfiles(self, request: Empty) -> Iterator[QoSProfile]: + 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 diff --git a/src/qos_profile/client/__init__.py b/src/qos_profile/client/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/qos_profile/client/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/qos_profile/requirements.in b/src/qos_profile/requirements.in new file mode 100644 index 0000000000000000000000000000000000000000..5cf553eaaec41de7599b6723e31e4ca3f82cbcae --- /dev/null +++ b/src/qos_profile/requirements.in @@ -0,0 +1,15 @@ +# 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. + + diff --git a/src/qos_profile/service/QoSProfileService.py b/src/qos_profile/service/QoSProfileService.py new file mode 100644 index 0000000000000000000000000000000000000000..bdc90f5bc0a12f5a1afbe59cf651439ee1f3219a --- /dev/null +++ b/src/qos_profile/service/QoSProfileService.py @@ -0,0 +1,28 @@ +# 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 common.Constants import ServiceNameEnum +from common.Settings import get_service_port_grpc +from common.proto.qos_profile_pb2_grpc import add_QoSProfileServiceServicer_to_server +from common.tools.service.GenericGrpcService import GenericGrpcService +from .QoSProfileServiceServicerImpl import QoSProfileServiceServicerImpl + +class QoSProfileService(GenericGrpcService): + def __init__(self, cls_name: str = __name__) -> None: + port = get_service_port_grpc(ServiceNameEnum.QOSPROFILE) + super().__init__(port, cls_name=cls_name) + self.qos_profile_servicer = QoSProfileServiceServicerImpl() + + def install_servicers(self): + add_QoSProfileServiceServicer_to_server(self.qos_profile_servicer, self.server) diff --git a/src/qos_profile/service/QoSProfileServiceServicerImpl.py b/src/qos_profile/service/QoSProfileServiceServicerImpl.py new file mode 100644 index 0000000000000000000000000000000000000000..38ad608d18471d29212e8c665450fb38da170cc3 --- /dev/null +++ b/src/qos_profile/service/QoSProfileServiceServicerImpl.py @@ -0,0 +1,77 @@ +# 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 grpc, logging +from typing import Iterator +from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method +from common.method_wrappers.ServiceExceptions import AlreadyExistsException +from common.proto.context_pb2 import Empty, QoSProfileId, QoSProfile +from common.proto.qos_profile_pb2_grpc import QoSProfileServiceServicer +from context.client.ContextClient import ContextClient + + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('QoSProfile', 'RPC') + +class QoSProfileServiceServicerImpl(QoSProfileServiceServicer): + def __init__(self ) -> None: + LOGGER.debug('Servicer Created') + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def CreateQoSProfile(self, request: QoSProfile, context: grpc.ServicerContext) -> QoSProfile: + context_client = ContextClient() + try: + qos_profile_get = context_client.GetQoSProfile(request.qos_profile_id) + except grpc.RpcError: + qos_profile_get = None + if qos_profile_get: + raise AlreadyExistsException('QoSProfile', request.qos_profile_id) + qos_profile = context_client.CreateQoSProfile(request) + return qos_profile + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def UpdateQoSProfile(self, request: QoSProfile, context: grpc.ServicerContext) -> QoSProfile: + context_client = ContextClient() + try: + _ = context_client.GetQoSProfile(request.qos_profile_id) + except grpc.RpcError as exc: + raise exc + qos_profile = context_client.UpdateQoSProfile(request) + return qos_profile + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def DeleteQoSProfile(self, request: QoSProfileId, context: grpc.ServicerContext) -> Empty: + context_client = ContextClient() + try: + _ = context_client.GetQoSProfile(request) + except grpc.RpcError as exc: + raise exc + empty = context_client.DeleteQoSProfile(request) + return empty + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def GetQoSProfile(self, request: QoSProfileId, context: grpc.ServicerContext) -> QoSProfile: + context_client = ContextClient() + try: + qos_profile = context_client.GetQoSProfile(request) + except grpc.RpcError as exc: + raise exc + return qos_profile + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def GetQoSProfiles(self, request: Empty, context: grpc.ServicerContext) -> Iterator[QoSProfile]: + context_client = ContextClient() + yield from context_client.GetQoSProfiles(request) + diff --git a/src/qos_profile/service/__init__.py b/src/qos_profile/service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/qos_profile/service/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/qos_profile/service/__main__.py b/src/qos_profile/service/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..ccd1ca23ffa4504569b44e6f2250ce583eafcf81 --- /dev/null +++ b/src/qos_profile/service/__main__.py @@ -0,0 +1,66 @@ +# 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, signal, sys, threading +from prometheus_client import start_http_server +from common.Constants import ServiceNameEnum +from common.Settings import ( + ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_log_level, get_metrics_port, + wait_for_environment_variables +) +from .QoSProfileService import QoSProfileService + +terminate = threading.Event() +LOGGER : logging.Logger = None + +def signal_handler(signal, frame): # pylint: disable=redefined-outer-name + LOGGER.warning('Terminate signal received') + terminate.set() + +def main(): + global LOGGER # pylint: disable=global-statement + + log_level = get_log_level() + logging.basicConfig(level=log_level, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s") + LOGGER = logging.getLogger(__name__) + + wait_for_environment_variables([ + get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST ), + get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC), + ]) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + LOGGER.info('Starting...') + + # Start metrics server + metrics_port = get_metrics_port() + start_http_server(metrics_port) + + # Starting service service + grpc_service = QoSProfileService() + 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/qos_profile/tests/.gitignore b/src/qos_profile/tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6b97d6fe3ad32f39097745229ab7f547f26ecb12 --- /dev/null +++ b/src/qos_profile/tests/.gitignore @@ -0,0 +1 @@ +# Add here your files containing confidential testbed details such as IP addresses, ports, usernames, passwords, etc. diff --git a/src/qos_profile/tests/__init__.py b/src/qos_profile/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/qos_profile/tests/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/qos_profile/tests/conftest.py b/src/qos_profile/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..37110c0b60aaf76655b44a8a79fb35aff77a8745 --- /dev/null +++ b/src/qos_profile/tests/conftest.py @@ -0,0 +1,23 @@ +# 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 pytest +from qos_profile.client.QoSProfileClient import QoSProfileClient + +@pytest.fixture(scope='function') +def qos_profile_client(): + _client = QoSProfileClient(host='0.0.0.0', port=30060) + yield _client + _client.close() + diff --git a/src/qos_profile/tests/test_crud.py b/src/qos_profile/tests/test_crud.py new file mode 100644 index 0000000000000000000000000000000000000000..1c5171da004b88c7a0b22345445c1a111c878d2f --- /dev/null +++ b/src/qos_profile/tests/test_crud.py @@ -0,0 +1,130 @@ +# 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 grpc import RpcError +import logging, pytest +from common.proto.context_pb2 import Empty, Uuid, QoSProfileValueUnitPair, QoSProfileId, QoSProfile + +from common.tools.grpc.Tools import grpc_message_to_json_string +from qos_profile.client.QoSProfileClient import QoSProfileClient + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +qos_profile_data = { + "qos_profile_id": "f00406f5-8e36-4abc-a0ec-b871c7f062b7", + "name": "QCI_1_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 +} + +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 test_create_qos_profile(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))) + assert qos_profile == qos_profile_created + + +def test_failed_create_qos_profile(qos_profile_client: QoSProfileClient): + qos_profile = create_qos_profile_from_json(qos_profile_data) + with pytest.raises(RpcError) as exc: + qos_profile_created = qos_profile_client.CreateQoSProfile(qos_profile) + +def test_get_qos_profile(qos_profile_client: QoSProfileClient): + qos_profile = create_qos_profile_from_json(qos_profile_data) + qos_profile_got = qos_profile_client.GetQoSProfile(qos_profile.qos_profile_id) + LOGGER.info('qos_profile_data = {:s}'.format(grpc_message_to_json_string(qos_profile_got))) + assert qos_profile == qos_profile_got + +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())) + LOGGER.info('qos_profile_data = {:s}'.format(grpc_message_to_json_string(qos_profiles_got))) + assert qos_profile == qos_profiles_got[0] + +def test_update_qos_profile(qos_profile_client: QoSProfileClient): + qos_profile = create_qos_profile_from_json(qos_profile_data) + qos_profile.packetErrorLossRate = 5 + qos_profile_updated = qos_profile_client.UpdateQoSProfile(qos_profile) + LOGGER.info('qos_profile_data = {:s}'.format(grpc_message_to_json_string(qos_profile_updated))) + assert qos_profile_updated.packetErrorLossRate == 5 + +def test_delete_qos_profiles(qos_profile_client: QoSProfileClient): + qos_profile = create_qos_profile_from_json(qos_profile_data) + with pytest.raises(RpcError) as exc: + qos_profiles_deleted = qos_profile_client.DeleteQoSProfile(QoSProfileId(qos_profile_id=Uuid(uuid='f8b1c625-ac01-405c-b1f7-b5ee06e16282'))) \ No newline at end of file