diff --git a/scripts/show_logs_pathcomp_backend.sh b/scripts/show_logs_pathcomp_backend.sh new file mode 100755 index 0000000000000000000000000000000000000000..cee99ee4bd19de9b7cd4e45eb651e809397ccaeb --- /dev/null +++ b/scripts/show_logs_pathcomp_backend.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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/pathcompservice -c backend diff --git a/scripts/show_logs_pathcomp_frontend.sh b/scripts/show_logs_pathcomp_frontend.sh new file mode 100755 index 0000000000000000000000000000000000000000..32f92b59d53b5804e9f1a0b145576667cfa21131 --- /dev/null +++ b/scripts/show_logs_pathcomp_frontend.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/) +# +# 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/pathcompservice -c frontend diff --git a/src/pathcomp/frontend/Config.py b/src/pathcomp/frontend/Config.py index 75f910e36f92ca008b318cfc61718ae81f94214c..f59ca45035ab0efecdf4297467626df18b74fa4d 100644 --- a/src/pathcomp/frontend/Config.py +++ b/src/pathcomp/frontend/Config.py @@ -16,13 +16,23 @@ import os DEFAULT_PATHCOMP_BACKEND_SCHEME = 'http' DEFAULT_PATHCOMP_BACKEND_HOST = '127.0.0.1' -DEFAULT_PATHCOMP_BACKEND_PORT = 8081 +DEFAULT_PATHCOMP_BACKEND_PORT = '8081' DEFAULT_PATHCOMP_BACKEND_BASEURL = '/pathComp/api/v1/compRoute' PATHCOMP_BACKEND_SCHEME = str(os.environ.get('PATHCOMP_BACKEND_SCHEME', DEFAULT_PATHCOMP_BACKEND_SCHEME )) -PATHCOMP_BACKEND_HOST = str(os.environ.get('PATHCOMP_BACKEND_HOST', DEFAULT_PATHCOMP_BACKEND_HOST )) -PATHCOMP_BACKEND_PORT = int(os.environ.get('PATHCOMP_BACKEND_PORT', DEFAULT_PATHCOMP_BACKEND_PORT )) PATHCOMP_BACKEND_BASEURL = str(os.environ.get('PATHCOMP_BACKEND_BASEURL', DEFAULT_PATHCOMP_BACKEND_BASEURL)) +# Find IP:port of backend container as follows: +# - first check env vars PATHCOMP_BACKEND_HOST & PATHCOMP_BACKEND_PORT +# - if not set, check env vars PATHCOMPSERVICE_SERVICE_HOST & PATHCOMPSERVICE_SERVICE_PORT_HTTP +# - if not set, use DEFAULT_PATHCOMP_BACKEND_HOST & DEFAULT_PATHCOMP_BACKEND_PORT +backend_host = DEFAULT_PATHCOMP_BACKEND_HOST +backend_host = os.environ.get('PATHCOMPSERVICE_SERVICE_HOST', backend_host) +PATHCOMP_BACKEND_HOST = str(os.environ.get('PATHCOMP_BACKEND_HOST', backend_host)) + +backend_port = DEFAULT_PATHCOMP_BACKEND_PORT +backend_port = os.environ.get('PATHCOMPSERVICE_SERVICE_PORT_HTTP', backend_port) +PATHCOMP_BACKEND_PORT = int(os.environ.get('PATHCOMP_BACKEND_PORT', backend_port)) + BACKEND_URL = '{:s}://{:s}:{:d}{:s}'.format( PATHCOMP_BACKEND_SCHEME, PATHCOMP_BACKEND_HOST, PATHCOMP_BACKEND_PORT, PATHCOMP_BACKEND_BASEURL) diff --git a/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py index 1e676b0aec93d083217bd53ca8e078b00ddf8376..c95f4e64a8660b437a1ae6721534c1c2f42f3303 100644 --- a/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py +++ b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py @@ -12,17 +12,73 @@ # See the License for the specific language governing permissions and # limitations under the License. -import copy +import operator from typing import Dict, List, Optional, Set, Tuple from common.proto.context_pb2 import Link -from common.proto.pathcomp_pb2 import Algorithm_KDisjointPath, Algorithm_KShortestPath, PathCompReply +from common.proto.pathcomp_pb2 import Algorithm_KDisjointPath, Algorithm_KShortestPath, PathCompReply, PathCompRequest +from common.tools.grpc.Tools import grpc_message_to_json_string from ._Algorithm import _Algorithm from .KShortestPathAlgorithm import KShortestPathAlgorithm +Service_Id = Tuple[str, str] # (context_uuid, service_uuid) +Service_Constraints = Dict[str, str] # {constraint_type => constraint_value} +Endpoint_Id = Tuple[str, str] # (device_uuid, endpoint_uuid) +Endpoint_Details = Tuple[str, int] # (site_id, priority) +Service_Endpoints = Dict[Endpoint_Id, Endpoint_Details] +Service_Details = Tuple[int, Service_Constraints, Service_Endpoints] +Services_Details = Dict[Service_Id, Service_Details] + +CUSTOM_CONSTRAINTS = {'bandwidth[gbps]', 'latency[ms]'} + +DUMP_EXECUTION_STEPS = False + class KDisjointPathAlgorithm(_Algorithm): def __init__(self, algorithm : Algorithm_KDisjointPath, class_name=__name__) -> None: super().__init__('KDP', False, class_name=class_name) self.num_disjoint = algorithm.num_disjoint + self.services_details : Services_Details = dict() + + def add_service_requests(self, request: PathCompRequest) -> None: + super().add_service_requests(request) + for service in request.services: + service_id = service.service_id + context_uuid = service_id.context_id.context_uuid.uuid + service_uuid = service_id.service_uuid.uuid + service_key = (context_uuid, service_uuid) + + constraints = dict() + endpoints = dict() + service_details = (int(service.service_type), constraints, endpoints) + self.services_details.setdefault(service_key, service_details) + + for constraint in service.service_constraints: + if constraint.WhichOneof('constraint') == 'custom': + constraint_type = constraint.custom.constraint_type + if constraint_type not in CUSTOM_CONSTRAINTS: continue + constraint_value = constraint.custom.constraint_value + constraints[constraint_type] = constraint_value + + if constraint.WhichOneof('constraint') == 'endpoint_location': + endpoint_id = constraint.endpoint_location.endpoint_id + device_uuid = endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = endpoint_id.endpoint_uuid.uuid + location_kind = constraint.endpoint_location.location.WhichOneof('location') + if location_kind != 'region': + MSG = 'Unsupported LocationType({:s}) in Constraint({:s})' + raise Exception(MSG.format(location_kind, grpc_message_to_json_string(constraint))) + site_id = constraint.endpoint_location.location.region + endpoints.setdefault((device_uuid, endpoint_uuid), dict())['site_id'] = site_id + + if constraint.WhichOneof('constraint') == 'endpoint_priority': + endpoint_id = constraint.endpoint_priority.endpoint_id + device_uuid = endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = endpoint_id.endpoint_uuid.uuid + priority = constraint.endpoint_priority.priority + endpoints.setdefault((device_uuid, endpoint_uuid), dict())['priority'] = priority + + # TODO: ensure these constraints are provided in the request + if 'bandwidth[gbps]' not in constraints: constraints['bandwidth[gbps]'] = '20.0' + if 'latency[ms]' not in constraints: constraints['latency[ms]'] = '20.0' def get_link_from_endpoint(self, endpoint : Dict) -> Tuple[Dict, Link]: device_uuid = endpoint['device_id'] @@ -50,8 +106,8 @@ class KDisjointPathAlgorithm(_Algorithm): def remove_traversed_links(self, link_list : List[Dict], path_endpoints : List[Dict]): _, path_link_ids = self.path_to_links(path_endpoints) new_link_list = list(filter(lambda l: l['link_Id'] not in path_link_ids, link_list)) - self.logger.info('cur_link_list = {:s}'.format(str(link_list))) - self.logger.info('new_link_list = {:s}'.format(str(new_link_list))) + #self.logger.info('cur_link_list = {:s}'.format(str(link_list))) + #self.logger.info('new_link_list = {:s}'.format(str(new_link_list))) return new_link_list def execute(self, dump_request_filename: Optional[str] = None, dump_reply_filename: Optional[str] = None) -> None: @@ -63,18 +119,52 @@ class KDisjointPathAlgorithm(_Algorithm): algorithm.link_list = self.link_list algorithm.link_dict = self.link_dict algorithm.endpoint_to_link_dict = self.endpoint_to_link_dict - algorithm.service_list = self.service_list - algorithm.service_dict = self.service_dict Path = List[Dict] Path_NoPath = Optional[Path] # None = no path, list = path self.json_reply : Dict[Tuple[str, str], List[Path_NoPath]] = dict() for num_path in range(self.num_disjoint): - #dump_request_filename = 'ksp-{:d}-request.json'.format(num_path) - #dump_reply_filename = 'ksp-{:d}-reply.txt'.format(num_path) - #algorithm.execute(dump_request_filename, dump_reply_filename) - algorithm.execute() + algorithm.service_list = list() + algorithm.service_dict = dict() + + #self.logger.warning('services_details = {:s}'.format(str(self.services_details))) + + _request = PathCompRequest() + for service_key, service_details in self.services_details.items(): + service_type, constraints, endpoints = service_details + _service = _request.services.add() + _service.service_id.context_id.context_uuid.uuid = service_key[0] + _service.service_id.service_uuid.uuid = service_key[1] + _service.service_type = service_type + for constraint_type, constraint_value in constraints.items(): + constraint = _service.service_constraints.add() + constraint.custom.constraint_type = constraint_type + constraint.custom.constraint_value = constraint_value + + site_to_endpoints : Dict[str, List[Tuple[Endpoint_Id, int]]] = {} + for endpoint_key,endpoint_details in endpoints.items(): + site_id = endpoint_details.get('site_id') + if site_id is None: continue + priority = endpoint_details.get('priority', 999) + site_to_endpoints.setdefault(site_id, list()).append((endpoint_key, priority)) + + for site_id,site_endpoints in site_to_endpoints.items(): + pending_endpoints = sorted(site_endpoints, key=operator.itemgetter(1)) + if len(pending_endpoints) == 0: continue + endpoint_key, _ = pending_endpoints[0] + device_uuid, endpoint_uuid = endpoint_key + endpoint_id = _service.service_endpoint_ids.add() + endpoint_id.device_id.device_uuid.uuid = device_uuid + endpoint_id.endpoint_uuid.uuid = endpoint_uuid + endpoints.pop(endpoint_key) + + algorithm.add_service_requests(_request) + + dump_request_filename = 'ksp-{:d}-request.json'.format(num_path) if DUMP_EXECUTION_STEPS else None + dump_reply_filename = 'ksp-{:d}-reply.txt'.format(num_path) if DUMP_EXECUTION_STEPS else None + algorithm.execute(dump_request_filename, dump_reply_filename) + response_list = algorithm.json_reply.get('response-list', []) for response in response_list: service_id = response['serviceId'] @@ -90,7 +180,7 @@ class KDisjointPathAlgorithm(_Algorithm): json_reply_service.append(path_endpoints) algorithm.link_list = self.remove_traversed_links(algorithm.link_list, path_endpoints) - self.logger.info('self.json_reply = {:s}'.format(str(self.json_reply))) + self.logger.debug('self.json_reply = {:s}'.format(str(self.json_reply))) def get_reply(self) -> PathCompReply: reply = PathCompReply() diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index d4973f168dd7bce9fb830600c2d010fc238f3b48..f90c1f18bae5e5d63cba5d8dd0785d8ba5f12d71 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -85,20 +85,21 @@ class _Algorithm: def execute(self, dump_request_filename : Optional[str] = None, dump_reply_filename : Optional[str] = None) -> None: request = {'serviceList': self.service_list, 'deviceList': self.device_list, 'linkList': self.link_list} + self.logger.debug('[execute] request={:s}'.format(str(request))) if dump_request_filename is not None: with open(dump_request_filename, 'w', encoding='UTF-8') as f: f.write(json.dumps(request, sort_keys=True, indent=4)) + self.logger.debug('[execute] BACKEND_URL: {:s}'.format(str(BACKEND_URL))) reply = requests.post(BACKEND_URL, json=request) self.status_code = reply.status_code self.raw_reply = reply.content.decode('UTF-8') + self.logger.debug('[execute] status_code={:s} reply={:s}'.format(str(reply.status_code), str(self.raw_reply))) if dump_reply_filename is not None: with open(dump_reply_filename, 'w', encoding='UTF-8') as f: f.write('status_code={:s} reply={:s}'.format(str(self.status_code), str(self.raw_reply))) - self.logger.info('status_code={:s} reply={:s}'.format(str(reply.status_code), str(self.raw_reply))) - if reply.status_code not in {requests.codes.ok}: raise Exception('Backend error({:s}) for request({:s})'.format( str(self.raw_reply), json.dumps(request, sort_keys=True))) @@ -125,8 +126,9 @@ class _Algorithm: _, grpc_service = tuple_service # TODO: implement support for multi-point services - service_endpoint_ids = grpc_service.service_endpoint_ids - if len(service_endpoint_ids) != 2: raise NotImplementedError('Service must have 2 endpoints') + # Control deactivated to enable disjoint paths with multiple redundant endpoints on each side + #service_endpoint_ids = grpc_service.service_endpoint_ids + #if len(service_endpoint_ids) != 2: raise NotImplementedError('Service must have 2 endpoints') service = reply.services.add() service.CopyFrom(grpc_service)