diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 115b336761dd94902597c3b6e21e7d3dcf225af1..ad0daad286c58e9d5da8480f9fd41ed66ab6efc8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,5 +49,7 @@ include: - local: '/src/kpi_value_api/.gitlab-ci.yml' - local: '/src/kpi_value_writer/.gitlab-ci.yml' - local: '/src/telemetry/.gitlab-ci.yml' + - local: '/src/qos_profile/.gitlab-ci.yml' + # This should be last one: end-to-end integration tests - local: '/src/tests/.gitlab-ci.yml' diff --git a/manifests/qos_profileservice.yaml b/manifests/qos_profileservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2ad7fa05538a253edc76072c2e817a52a01eeea9 --- /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: 20040 + - containerPort: 9192 + env: + - name: LOG_LEVEL + value: "INFO" + readinessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:20040"] + livenessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:20040"] + 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: 20040 + targetPort: 20040 + - name: metrics + protocol: TCP + port: 9192 + targetPort: 9192 diff --git a/proto/context.proto b/proto/context.proto index 87f69132df022e2aa4a0766dc9f0a7a7fae36d59..7f99c4525d8ce2e7c3d4e3b4b2f82fed6d861e96 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -68,6 +68,12 @@ service ContextService { rpc GetSliceEvents (Empty ) returns (stream SliceEvent ) {} rpc SelectSlice (SliceFilter ) returns ( SliceList ) {} + rpc CreateQoSProfile (QoSProfile ) returns ( QoSProfile ) {} + rpc UpdateQoSProfile (QoSProfile ) returns ( QoSProfile ) {} + rpc DeleteQoSProfile (QoSProfileId ) returns ( Empty ) {} + rpc GetQoSProfile (QoSProfileId ) returns ( QoSProfile ) {} + rpc GetQoSProfiles (Empty ) returns (stream QoSProfile ) {} + rpc ListConnectionIds (ServiceId ) returns ( ConnectionIdList) {} rpc ListConnections (ServiceId ) returns ( ConnectionList ) {} rpc GetConnection (ConnectionId ) returns ( Connection ) {} @@ -402,6 +408,43 @@ message SliceEvent { SliceId slice_id = 2; } + +// ----- QoSProfile ---------------------------------------------------------------------------------------------------- +message QoSProfileId { + Uuid qos_profile_id = 1; +} + +message QoSProfileValueUnitPair { + int32 value = 1; + string unit = 2; +} + +message QoDConstraintsRequest { + QoSProfileId qos_profile_id = 1; + double start_timestamp = 2; + float duration = 3; +} + +message QoSProfile { + QoSProfileId qos_profile_id = 1; + string name = 2; + string description = 3; + string status = 4; + QoSProfileValueUnitPair targetMinUpstreamRate = 5; + QoSProfileValueUnitPair maxUpstreamRate = 6; + QoSProfileValueUnitPair maxUpstreamBurstRate = 7; + QoSProfileValueUnitPair targetMinDownstreamRate = 8; + QoSProfileValueUnitPair maxDownstreamRate = 9; + QoSProfileValueUnitPair maxDownstreamBurstRate = 10; + QoSProfileValueUnitPair minDuration = 11; + QoSProfileValueUnitPair maxDuration = 12; + int32 priority = 13; + QoSProfileValueUnitPair packetDelayBudget = 14; + QoSProfileValueUnitPair jitter = 15; + int32 packetErrorLossRate = 16; +} + + // ----- Connection ---------------------------------------------------------------------------------------------------- message ConnectionId { Uuid connection_uuid = 1; @@ -534,7 +577,7 @@ message Constraint_Custom { } message Constraint_Schedule { - float start_timestamp = 1; + double start_timestamp = 1; float duration_days = 2; } @@ -597,6 +640,22 @@ message Constraint_Exclusions { repeated LinkId link_ids = 4; } +message Constraint_QoSProfile { + QoSProfileValueUnitPair target_min_upstream_rate = 1; + QoSProfileValueUnitPair max_upstream_rate = 2; + QoSProfileValueUnitPair max_upstream_burst_rate = 3; + QoSProfileValueUnitPair target_min_downstream_rate = 4; + QoSProfileValueUnitPair max_downstream_rate = 5; + QoSProfileValueUnitPair max_downstream_burst_rate = 6; + QoSProfileValueUnitPair min_duration = 7; + QoSProfileValueUnitPair max_duration = 8; + int32 priority = 9; + QoSProfileValueUnitPair packet_delay_budget = 10; + QoSProfileValueUnitPair jitter = 11; + int32 packet_error_loss_rate = 12; + +} + message Constraint { ConstraintActionEnum action = 1; oneof constraint { @@ -609,6 +668,7 @@ message Constraint { Constraint_SLA_Availability sla_availability = 8; Constraint_SLA_Isolation_level sla_isolation = 9; Constraint_Exclusions exclusions = 10; + Constraint_QoSProfile qos_profile = 11; } } diff --git a/proto/qos_profile.proto b/proto/qos_profile.proto new file mode 100644 index 0000000000000000000000000000000000000000..557cef5391077043e3367054768ce87ef5f32d97 --- /dev/null +++ b/proto/qos_profile.proto @@ -0,0 +1,27 @@ +// 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. + +syntax = "proto3"; +package qos_profile; + +import "context.proto"; + +service QoSProfileService { + rpc CreateQoSProfile (context.QoSProfile ) returns (context.QoSProfile ) {} + rpc UpdateQoSProfile (context.QoSProfile ) returns (context.QoSProfile ) {} + rpc DeleteQoSProfile (context.QoSProfileId ) returns (context.Empty ) {} + rpc GetQoSProfile (context.QoSProfileId ) returns (context.QoSProfile ) {} + rpc GetQoSProfiles (context.Empty ) returns (stream context.QoSProfile ) {} + rpc GetConstraintListFromQoSProfile (context.QoDConstraintsRequest) returns (stream context.Constraint ) {} +} diff --git a/scripts/show_logs_qos_profile.sh b/scripts/show_logs_qos_profile.sh new file mode 100755 index 0000000000000000000000000000000000000000..744bed9a62af6c376e94d7d13492b796821ecc53 --- /dev/null +++ b/scripts/show_logs_qos_profile.sh @@ -0,0 +1,27 @@ +#!/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. + +######################################################################################################################## +# Define your deployment settings here +######################################################################################################################## + +# If not already set, set the name of the Kubernetes namespace to deploy to. +export TFS_K8S_NAMESPACE=${TFS_K8S_NAMESPACE:-"tfs"} + +######################################################################################################################## +# Automated steps start here +######################################################################################################################## + +kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/qos_profileservice -c server diff --git a/src/common/Constants.py b/src/common/Constants.py index 4b2bced95fca28abdfd729492acc1117cdf3e8d9..1bfde1dd9e62ec39969bbd83135fe77c33e03e19 100644 --- a/src/common/Constants.py +++ b/src/common/Constants.py @@ -66,6 +66,7 @@ class ServiceNameEnum(Enum): KPIVALUEWRITER = 'kpi-value-writer' TELEMETRYFRONTEND = 'telemetry-frontend' TELEMETRYBACKEND = 'telemetry-backend' + QOSPROFILE = 'qos-profile' # Used for test and debugging only DLT_GATEWAY = 'dltgateway' @@ -95,6 +96,7 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.E2EORCHESTRATOR .value : 10050, ServiceNameEnum.OPTICALCONTROLLER .value : 10060, ServiceNameEnum.BGPLS .value : 20030, + ServiceNameEnum.QOSPROFILE .value : 20040, ServiceNameEnum.KPIMANAGER .value : 30010, ServiceNameEnum.KPIVALUEAPI .value : 30020, ServiceNameEnum.KPIVALUEWRITER .value : 30030, diff --git a/src/context/client/ContextClient.py b/src/context/client/ContextClient.py index 2776a0d294e9a9ee7b00e46bfd3fbb068133741f..024381c54a8de6ec9c305f8ac6ecd8e584e33983 100644 --- a/src/context/client/ContextClient.py +++ b/src/context/client/ContextClient.py @@ -27,7 +27,7 @@ from common.proto.context_pb2 import ( Service, ServiceEvent, ServiceFilter, ServiceId, ServiceIdList, ServiceList, Slice, SliceEvent, SliceFilter, SliceId, SliceIdList, SliceList, Topology, TopologyDetails, TopologyEvent, TopologyId, TopologyIdList, TopologyList, - OpticalConfig, OpticalConfigId, OpticalConfigList + OpticalConfig, OpticalConfigId, OpticalConfigList, QoSProfileId, QoSProfile ) from common.proto.context_pb2_grpc import ContextServiceStub from common.proto.context_policy_pb2_grpc import ContextPolicyServiceStub @@ -362,6 +362,41 @@ class ContextClient: LOGGER.debug('GetSliceEvents result: {:s}'.format(grpc_message_to_json_string(response))) return response + @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 + @RETRY_DECORATOR def ListConnectionIds(self, request: ServiceId) -> ConnectionIdList: LOGGER.debug('ListConnectionIds request: {:s}'.format(grpc_message_to_json_string(request))) diff --git a/src/context/service/ContextServiceServicerImpl.py b/src/context/service/ContextServiceServicerImpl.py index be32372108e059625801d14c660d18cbe0df677f..e6c305f2f68b2638047a3f945da26929a0d1c48e 100644 --- a/src/context/service/ContextServiceServicerImpl.py +++ b/src/context/service/ContextServiceServicerImpl.py @@ -24,7 +24,7 @@ from common.proto.context_pb2 import ( Service, ServiceEvent, ServiceFilter, ServiceId, ServiceIdList, ServiceList, Slice, SliceEvent, SliceFilter, SliceId, SliceIdList, SliceList, Topology, TopologyDetails, TopologyEvent, TopologyId, TopologyIdList, TopologyList, - OpticalConfigList, OpticalConfigId, OpticalConfig + OpticalConfigList, OpticalConfigId, OpticalConfig, QoSProfileId, QoSProfile ) from common.proto.policy_pb2 import PolicyRuleIdList, PolicyRuleId, PolicyRuleList, PolicyRule from common.proto.context_pb2_grpc import ContextServiceServicer @@ -46,6 +46,7 @@ from .database.Slice import ( from .database.Topology import ( topology_delete, topology_get, topology_get_details, topology_list_ids, topology_list_objs, topology_set) from .database.OpticalConfig import set_opticalconfig, select_opticalconfig, get_opticalconfig +from .database.QoSProfile import set_qos_profile, delete_qos_profile, get_qos_profile, get_qos_profiles LOGGER = logging.getLogger(__name__) @@ -251,6 +252,34 @@ class ContextServiceServicerImpl(ContextServiceServicer, ContextPolicyServiceSer for message in consume_events(self.messagebroker, {EventTopicEnum.SLICE}): yield message + # ----- QoSProfile ----------------------------------------------------------------------------------------------- + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def CreateQoSProfile(self, request : QoSProfile, context : grpc.ServicerContext) -> QoSProfile: + return set_qos_profile(self.db_engine, request) + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def UpdateQoSProfile(self, request : QoSProfile, context : grpc.ServicerContext) -> QoSProfile: + return set_qos_profile(self.db_engine, request) + + @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.qos_profile_id.uuid) + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def GetQoSProfile(self, request : QoSProfileId, context : grpc.ServicerContext) -> QoSProfile: + qos_profile = get_qos_profile(self.db_engine, request.qos_profile_id.uuid) + if qos_profile is None: + context.set_details(f'QoSProfile {request.qos_profile_id.uuid} not found') + context.set_code(grpc.StatusCode.NOT_FOUND) + return QoSProfile() + 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) + + # ----- Connection ------------------------------------------------------------------------------------------------- @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) diff --git a/src/context/service/database/QoSProfile.py b/src/context/service/database/QoSProfile.py new file mode 100644 index 0000000000000000000000000000000000000000..33f6cc6ec937584c2b51e5efd881764095827285 --- /dev/null +++ b/src/context/service/database/QoSProfile.py @@ -0,0 +1,116 @@ +# 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 sqlalchemy.dialects.postgresql import insert +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy_cockroachdb import run_transaction +from typing import List, Optional + +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 + +LOGGER = logging.getLogger(__name__) + +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 qos_table_data_to_grpc_message(data: QoSProfileModel) -> QoSProfile: + return QoSProfile( + qos_profile_id = QoSProfileId(qos_profile_id=Uuid(uuid=data.qos_profile_id)), + name = data.name, + description = data.description, + status = data.status, + targetMinUpstreamRate = QoSProfileValueUnitPair(**data.targetMinUpstreamRate), + maxUpstreamRate = QoSProfileValueUnitPair(**data.maxUpstreamRate), + maxUpstreamBurstRate = QoSProfileValueUnitPair(**data.maxUpstreamBurstRate), + targetMinDownstreamRate = QoSProfileValueUnitPair(**data.targetMinDownstreamRate), + maxDownstreamRate = QoSProfileValueUnitPair(**data.maxDownstreamRate), + maxDownstreamBurstRate = QoSProfileValueUnitPair(**data.maxDownstreamBurstRate), + minDuration = QoSProfileValueUnitPair(**data.minDuration), + maxDuration = QoSProfileValueUnitPair(**data.maxDuration), + priority = data.priority, + packetDelayBudget = QoSProfileValueUnitPair(**data.packetDelayBudget), + jitter = QoSProfileValueUnitPair(**data.jitter), + packetErrorLossRate = data.packetErrorLossRate + ) + +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]) + 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: + num_deleted = session.query(QoSProfileModel).filter_by(qos_profile_id=request).delete() + return num_deleted > 0 + deleted = run_transaction(sessionmaker(bind=db_engine), callback) + return Empty() + +def get_qos_profile(db_engine : Engine, request : str) -> Optional[QoSProfile]: + def callback(session : Session) -> Optional[QoSProfile]: + obj : Optional[QoSProfileModel] = session.query(QoSProfileModel).filter_by(qos_profile_id=request).one_or_none() + return None if obj is None else qos_table_data_to_grpc_message(obj) + return run_transaction(sessionmaker(bind=db_engine), callback) + +def get_qos_profiles(db_engine : Engine, request : Empty) -> List[QoSProfile]: + def callback(session : Session) -> List[QoSProfile]: + obj_list : List[QoSProfileModel] = session.query(QoSProfileModel).all() + return [qos_table_data_to_grpc_message(obj) for obj in obj_list] + return run_transaction(sessionmaker(bind=db_engine), callback) diff --git a/src/context/service/database/models/QoSProfile.py b/src/context/service/database/models/QoSProfile.py new file mode 100644 index 0000000000000000000000000000000000000000..431d0f503ce7c5224bff108d570c953ff6f538c6 --- /dev/null +++ b/src/context/service/database/models/QoSProfile.py @@ -0,0 +1,37 @@ +# 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 sqlalchemy import Column, Integer, String, JSON +from ._Base import _Base + + +class QoSProfileModel(_Base): + __tablename__ = 'qos_profile' + + qos_profile_id = Column(String, primary_key=True) + name = Column(String, nullable=False) + description = Column(String, nullable=False) + status = Column(String, nullable=False) + targetMinUpstreamRate = Column(JSON, nullable=False) + maxUpstreamRate = Column(JSON, nullable=False) + maxUpstreamBurstRate = Column(JSON, nullable=False) + targetMinDownstreamRate = Column(JSON, nullable=False) + maxDownstreamRate = Column(JSON, nullable=False) + maxDownstreamBurstRate = Column(JSON, nullable=False) + minDuration = Column(JSON, nullable=False) + maxDuration = Column(JSON, nullable=False) + priority = Column(Integer, nullable=False) + packetDelayBudget = Column(JSON, nullable=False) + jitter = Column(JSON, nullable=False) + packetErrorLossRate = Column(Integer, nullable=False) diff --git a/src/qos_profile/.gitlab-ci.yml b/src/qos_profile/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..29ac9f3b4ee532b0ab6f6ab5523139180849126b --- /dev/null +++ b/src/qos_profile/.gitlab-ci.yml @@ -0,0 +1,151 @@ +# 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. + +# Build, tag, and push the Docker image to the GitLab Docker registry +build qos_profile: + variables: + IMAGE_NAME: "qos_profile" # name of the microservice + IMAGE_TAG: "latest" # tag of the container image (production, development, etc) + stage: build + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - docker buildx build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile . + - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/$IMAGE_NAME/**/*.{py,in,yml} + - src/$IMAGE_NAME/Dockerfile + - src/$IMAGE_NAME/tests/*.py + - manifests/${IMAGE_NAME}service.yaml + - .gitlab-ci.yml + +# Apply unit test to the component +unit_test qos_profile: + variables: + IMAGE_NAME: "qos_profile" # name of the microservice + IMAGE_TAG: "latest" # tag of the container image (production, development, etc) + stage: unit_test + needs: + - build qos_profile + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create --driver=bridge teraflowbridge; fi + + # Context-related + - if docker container ls | grep crdb; then docker rm -f crdb; else echo "CockroachDB container is not in the system"; fi + - if docker volume ls | grep crdb; then docker volume rm -f crdb; else echo "CockroachDB volume is not in the system"; fi + - if docker container ls | grep nats; then docker rm -f nats; else echo "NATS container is not in the system"; fi + + # Context-related + - if docker container ls | grep context; then docker rm -f context; else echo "context image is not in the system"; fi + + # QoSProfile-related + - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME image is not in the system"; fi + + script: + - docker pull "cockroachdb/cockroach:latest-v22.2" + - docker pull "nats:2.9" + - docker pull "$CI_REGISTRY_IMAGE/context:$IMAGE_TAG" + - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + + # Context preparation + - docker volume create crdb + - > + docker run --name crdb -d --network=teraflowbridge -p 26257:26257 -p 8080:8080 + --env COCKROACH_DATABASE=tfs_test --env COCKROACH_USER=tfs --env COCKROACH_PASSWORD=tfs123 + --volume "crdb:/cockroach/cockroach-data" + cockroachdb/cockroach:latest-v22.2 start-single-node + - > + docker run --name nats -d --network=teraflowbridge -p 4222:4222 -p 8222:8222 + nats:2.9 --http_port 8222 --user tfs --pass tfs123 + - echo "Waiting for initialization..." + - while ! docker logs crdb 2>&1 | grep -q 'finished creating default user \"tfs\"'; do sleep 1; done + - docker logs crdb + - while ! docker logs nats 2>&1 | grep -q 'Server is ready'; do sleep 1; done + - docker logs nats + - docker ps -a + - CRDB_ADDRESS=$(docker inspect crdb --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") + - echo $CRDB_ADDRESS + - NATS_ADDRESS=$(docker inspect nats --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") + - echo $NATS_ADDRESS + - > + docker run --name context -d -p 1010:1010 + --env "CRDB_URI=cockroachdb://tfs:tfs123@${CRDB_ADDRESS}:26257/tfs_test?sslmode=require" + --env "MB_BACKEND=nats" + --env "NATS_URI=nats://tfs:tfs123@${NATS_ADDRESS}:4222" + --network=teraflowbridge + $CI_REGISTRY_IMAGE/context:$IMAGE_TAG + - CONTEXTSERVICE_SERVICE_HOST=$(docker inspect context --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") + - echo $CONTEXTSERVICE_SERVICE_HOST + + # QoSProfile preparation + - > + docker run --name $IMAGE_NAME -d -p 3030:3030 + --env "CONTEXTSERVICE_SERVICE_HOST=${CONTEXTSERVICE_SERVICE_HOST}" + --volume "$PWD/src/$IMAGE_NAME/tests:/opt/results" + --network=teraflowbridge + $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + + # Check status before the tests + - sleep 5 + - docker ps -a + - docker logs context + - docker logs $IMAGE_NAME + + # Run the tests + - > + docker exec -i $IMAGE_NAME bash -c + "coverage run -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_crud.py --junitxml=/opt/results/${IMAGE_NAME}_report.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: + # Check status after the tests + - docker ps -a + - docker logs context + - docker logs $IMAGE_NAME + + - docker rm -f $IMAGE_NAME + - docker rm -f context + + - docker rm -f $IMAGE_NAME crdb nats + - docker volume rm -f crdb + - docker network rm teraflowbridge + - docker volume prune --force + - docker image prune --force + + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/$IMAGE_NAME/**/*.{py,in,yml} + - src/$IMAGE_NAME/Dockerfile + - src/$IMAGE_NAME/tests/*.py + - manifests/${IMAGE_NAME}service.yaml + - .gitlab-ci.yml + + artifacts: + when: always + reports: + junit: src/$IMAGE_NAME/tests/${IMAGE_NAME}_report.xml 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..e7bec8739ca1b374d79694c14b3c6fa511d7079a --- /dev/null +++ b/src/qos_profile/client/QoSProfileClient.py @@ -0,0 +1,89 @@ +# 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, QoDConstraintsRequest, Constraint +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 + + @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))) + return response \ No newline at end of file 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..bdcc3e8c3c17544b1c8e05ecbf64d6107e208b2b --- /dev/null +++ b/src/qos_profile/service/QoSProfileServiceServicerImpl.py @@ -0,0 +1,122 @@ +# 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 + +import grpc._channel +from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method +from common.proto.context_pb2 import QoDConstraintsRequest, Constraint, ConstraintActionEnum, Constraint_QoSProfile, Constraint_Schedule, 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) + context.set_details(f'QoSProfile {request.qos_profile_id.qos_profile_id.uuid} already exists') + context.set_code(grpc.StatusCode.ALREADY_EXISTS) + return QoSProfile() + except grpc._channel._InactiveRpcError as exc: + if exc.code() != grpc.StatusCode.NOT_FOUND: + raise exc + 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._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + context.set_details(f'QoSProfile {request.qos_profile_id.qos_profile_id.uuid} not found') + context.set_code(grpc.StatusCode.NOT_FOUND) + return QoSProfile() + 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._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + context.set_details(f'QoSProfile {request.qos_profile_id.uuid} not found') + context.set_code(grpc.StatusCode.NOT_FOUND) + return QoSProfile() + 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._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + context.set_details(f'QoSProfile {request.qos_profile_id.uuid} not found') + context.set_code(grpc.StatusCode.NOT_FOUND) + return QoSProfile() + 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) + + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def GetConstraintListFromQoSProfile(self, request: QoDConstraintsRequest, context: grpc.ServicerContext) -> Iterator[Constraint]: + context_client = ContextClient() + try: + qos_profile = context_client.GetQoSProfile(request.qos_profile_id) + except grpc._channel._InactiveRpcError as exc: + if exc.code() == grpc.StatusCode.NOT_FOUND: + context.set_details(f'QoSProfile {request.qos_profile_id.qos_profile_id.uuid} not found') + context.set_code(grpc.StatusCode.NOT_FOUND) + yield Constraint() + + qos_profile_constraint = Constraint_QoSProfile() + qos_profile_constraint.target_min_upstream_rate.CopyFrom(qos_profile.targetMinUpstreamRate) + qos_profile_constraint.max_upstream_rate.CopyFrom(qos_profile.maxUpstreamRate) + qos_profile_constraint.max_upstream_burst_rate.CopyFrom(qos_profile.maxUpstreamBurstRate) + qos_profile_constraint.target_min_downstream_rate.CopyFrom(qos_profile.targetMinDownstreamRate) + qos_profile_constraint.max_downstream_rate.CopyFrom(qos_profile.maxDownstreamRate) + qos_profile_constraint.max_downstream_burst_rate.CopyFrom(qos_profile.maxDownstreamBurstRate) + qos_profile_constraint.min_duration.CopyFrom(qos_profile.minDuration) + qos_profile_constraint.max_duration.CopyFrom(qos_profile.maxDuration) + qos_profile_constraint.priority = qos_profile.priority + qos_profile_constraint.packet_delay_budget.CopyFrom(qos_profile.packetDelayBudget) + qos_profile_constraint.jitter.CopyFrom(qos_profile.jitter) + qos_profile_constraint.packet_error_loss_rate =qos_profile.packetErrorLossRate + 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 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..5c95fd62c853896dc3c5e8ed9c2f1543186a59a6 --- /dev/null +++ b/src/qos_profile/tests/conftest.py @@ -0,0 +1,47 @@ +# 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 +from common.proto.context_pb2 import Uuid, QoSProfileValueUnitPair, QoSProfileId, QoSProfile + +@pytest.fixture(scope='function') +def qos_profile_client(): + _client = QoSProfileClient(host='0.0.0.0', port=20040) + yield _client + _client.close() + + + +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 \ No newline at end of file diff --git a/src/qos_profile/tests/test_constraints.py b/src/qos_profile/tests/test_constraints.py new file mode 100644 index 0000000000000000000000000000000000000000..51f160fc5a21fa3f914872f356825a08e653a6b5 --- /dev/null +++ b/src/qos_profile/tests/test_constraints.py @@ -0,0 +1,142 @@ +# 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 google.protobuf.json_format import MessageToDict + +from common.proto.context_pb2 import QoDConstraintsRequest +from common.tools.grpc.Tools import grpc_message_to_json_string +from qos_profile.client.QoSProfileClient import QoSProfileClient + +from .conftest import create_qos_profile_from_json + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +qos_profile_data = { + "qos_profile_id": "0afc905f-f1f0-4ae2-9925-9df17140b8bf", + "name": "QCI_2_voice", + "description": "QoS profile for game streaming", + "status": "ACTIVE", + "targetMinUpstreamRate": { + "value": 5, + "unit": "bps" + }, + "maxUpstreamRate": { + "value": 5, + "unit": "bps" + }, + "maxUpstreamBurstRate": { + "value": 5, + "unit": "bps" + }, + "targetMinDownstreamRate": { + "value": 5, + "unit": "bps" + }, + "maxDownstreamRate": { + "value": 5, + "unit": "bps" + }, + "maxDownstreamBurstRate": { + "value": 5, + "unit": "bps" + }, + "minDuration": { + "value": 5, + "unit": "Minutes" + }, + "maxDuration": { + "value": 6, + "unit": "Minutes" + }, + "priority": 5, + "packetDelayBudget": { + "value": 5, + "unit": "Minutes" + }, + "jitter": { + "value": 5, + "unit": "Minutes" + }, + "packetErrorLossRate": 3 +} + +target_qos_profile_constraint = { + "action": "CONSTRAINTACTION_SET", + "qos_profile": { + "target_min_upstream_rate": { + "value": 5, + "unit": "bps" + }, + "max_upstream_rate": { + "value": 5, + "unit": "bps" + }, + "max_upstream_burst_rate": { + "value": 5, + "unit": "bps" + }, + "target_min_downstream_rate": { + "value": 5, + "unit": "bps" + }, + "max_downstream_rate": { + "value": 5, + "unit": "bps" + }, + "max_downstream_burst_rate": { + "value": 5, + "unit": "bps" + }, + "min_duration": { + "value": 5, + "unit": "Minutes" + }, + "max_duration": { + "value": 6, + "unit": "Minutes" + }, + "priority": 5, + "packet_delay_budget": { + "value": 5, + "unit": "Minutes" + }, + "jitter": { + "value": 5, + "unit": "Minutes" + }, + "packet_error_loss_rate": 3 + } +} + + +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 + assert constraint_1.WhichOneof('constraint') == 'qos_profile' + print(MessageToDict(constraint_1, preserving_proto_field_name=True)) + assert MessageToDict(constraint_1, preserving_proto_field_name=True) == target_qos_profile_constraint + assert constraint_2.WhichOneof('constraint') == 'schedule' + assert constraint_2.schedule.start_timestamp == 1726063284.25332 + assert constraint_2.schedule.duration_days == 1 + + qos_profile_client.DeleteQoSProfile(qos_profile.qos_profile_id) \ No newline at end of file diff --git a/src/qos_profile/tests/test_crud.py b/src/qos_profile/tests/test_crud.py new file mode 100644 index 0000000000000000000000000000000000000000..b98351ce9ae0d7da5151702d5b76c436b4844928 --- /dev/null +++ b/src/qos_profile/tests/test_crud.py @@ -0,0 +1,115 @@ +# 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, StatusCode +import logging, pytest +from .conftest import create_qos_profile_from_json +from common.proto.context_pb2 import Empty, Uuid, QoSProfileId +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 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) + assert exc.value.code() == StatusCode.ALREADY_EXISTS + +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_failed_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'))) + assert exc.value.code() == StatusCode.NOT_FOUND + +def test_delete_qos_profiles(qos_profile_client: QoSProfileClient): + qos_profile = create_qos_profile_from_json(qos_profile_data) + qos_profiles_deleted = qos_profile_client.DeleteQoSProfile(qos_profile.qos_profile_id) + assert qos_profiles_deleted == Empty() \ No newline at end of file