diff --git a/manifests/pathcompservice.yaml b/manifests/pathcompservice.yaml index 0b4e561cabe810b451534e433785bcfadf388663..138191e2a619a3b7c67d2fbdcbebaa284e00fbc8 100644 --- a/manifests/pathcompservice.yaml +++ b/manifests/pathcompservice.yaml @@ -93,3 +93,7 @@ spec: protocol: TCP port: 10020 targetPort: 10020 + - name: http + protocol: TCP + port: 8081 + targetPort: 8081 diff --git a/proto/pathcomp.proto b/proto/pathcomp.proto index 9eb650fb9981b4b84f31b63796eec0c7a8e780b6..08f33efe99b6a25c568c8be14f1355b3d4521909 100644 --- a/proto/pathcomp.proto +++ b/proto/pathcomp.proto @@ -28,11 +28,16 @@ message Algorithm_KShortestPath { uint32 k_return = 2; } +message Algorithm_KDisjointPath { + uint32 num_disjoint = 1; +} + message PathCompRequest { repeated context.Service services = 1; oneof algorithm { Algorithm_ShortestPath shortest_path = 10; Algorithm_KShortestPath k_shortest_path = 11; + Algorithm_KDisjointPath k_disjoint_path = 12; } } diff --git a/src/pathcomp/frontend/service/PathCompServiceServicerImpl.py b/src/pathcomp/frontend/service/PathCompServiceServicerImpl.py index b1da515631320d7d77dee073e8985a8d63bd3c30..1d55646abffcdb4a882167406ba046aca7bfa651 100644 --- a/src/pathcomp/frontend/service/PathCompServiceServicerImpl.py +++ b/src/pathcomp/frontend/service/PathCompServiceServicerImpl.py @@ -12,17 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import grpc, json, logging, requests, uuid -from typing import Dict, Tuple -from common.proto.context_pb2 import Empty, EndPointId, Service +import grpc, logging +from common.proto.context_pb2 import Empty from common.proto.pathcomp_pb2 import PathCompReply, PathCompRequest from common.proto.pathcomp_pb2_grpc import PathCompServiceServicer from common.rpc_method_wrapper.Decorator import create_metrics, safe_and_metered_rpc_method from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient -from pathcomp.frontend.Config import BACKEND_URL -from pathcomp.frontend.service.tools.ComposeRequest import compose_device, compose_link, compose_service -#from pathcomp.frontend.service.tools.Constants import CapacityUnit +from pathcomp.frontend.service.algorithms.Factory import get_algorithm LOGGER = logging.getLogger(__name__) @@ -39,123 +36,26 @@ class PathCompServiceServicerImpl(PathCompServiceServicer): def Compute(self, request : PathCompRequest, context : grpc.ServicerContext) -> PathCompReply: LOGGER.info('[Compute] begin ; request = {:s}'.format(grpc_message_to_json_string(request))) - algorithm = request.WhichOneof('algorithm') - if algorithm == 'shortest_path': - # no attributes - pass - elif algorithm == 'k_shortest_path': - k_inspection = request.k_shortest_path.k_inspection - k_return = request.k_shortest_path.k_return - else: - raise NotImplementedError('Unsupported Algorithm: {:s}'.format(str(algorithm))) - context_client = ContextClient() - algorithm = {'id': 'KSP', 'sync': False, 'k_paths': k_return} - service_list = [ - compose_service(grpc_service, algorithm) - for grpc_service in request.services - ] - get_service_key = lambda service_id: (service_id['contextId'], service_id['service_uuid']) - service_dict : Dict[Tuple[str, str], Tuple[Dict, Service]] = { - get_service_key(json_service['serviceId']): (json_service, grpc_service) - for json_service,grpc_service in zip(service_list, request.services) - } - #LOGGER.info('service_dict = {:s}'.format(str(service_dict))) - - # TODO: consider filtering resources - - #grpc_contexts = context_client.ListContexts(Empty()) - #for grpc_context in grpc_contexts.contexts: - # # TODO: add context to request - # grpc_topologies = context_client.ListTopologies(grpc_context.context_id) - # for grpc_topology in grpc_topologies.topologies: #pylint: disable=unused-variable - # # TODO: add topology to request - # pass - - grpc_devices = context_client.ListDevices(Empty()) - device_list = [ - compose_device(grpc_device) - for grpc_device in grpc_devices.devices - ] - endpoint_dict : Dict[str, Dict[str, Tuple[Dict, EndPointId]]] = { - json_device['device_Id']: { - json_endpoint['endpoint_id']['endpoint_uuid']: (json_endpoint['endpoint_id'], grpc_endpoint.endpoint_id) - for json_endpoint,grpc_endpoint in zip(json_device['device_endpoints'], grpc_device.device_endpoints) - } - for json_device,grpc_device in zip(device_list, grpc_devices.devices) - } - #LOGGER.info('endpoint_dict = {:s}'.format(str(endpoint_dict))) - - grpc_links = context_client.ListLinks(Empty()) - link_list = [ - compose_link(grpc_link) - for grpc_link in grpc_links.links - ] - - request = { - 'serviceList': service_list, - 'deviceList' : device_list, - 'linkList' : link_list, - } - - #with open('pc-req.json', 'w', encoding='UTF-8') as f: - # f.write(json.dumps(request, sort_keys=True, indent=4)) - - reply = requests.post(BACKEND_URL, json=request) - if reply.status_code not in {requests.codes.ok}: - raise Exception('Backend error({:s}) for request({:s})'.format( - str(reply.content.decode('UTF-8')), json.dumps(request, sort_keys=True))) - LOGGER.info('status_code={:s} reply={:s}'.format( - str(reply.status_code), str(reply.content.decode('UTF-8')))) - - json_reply = reply.json() - response_list = json_reply.get('response-list', []) - reply = PathCompReply() - for response in response_list: - service_key = get_service_key(response['serviceId']) - tuple_service = service_dict.get(service_key) - if tuple_service is None: raise Exception('ServiceKey({:s}) not found'.format(str(service_key))) - json_service, 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') - - service = reply.services.add() - service.CopyFrom(grpc_service) - - connection = reply.connections.add() - connection.connection_id.connection_uuid.uuid = str(uuid.uuid4()) - connection.service_id.CopyFrom(service.service_id) - - no_path_issue = response.get('noPath', {}).get('issue') - if no_path_issue is not None: - # no path found: leave connection with no endpoints - # no_path_issue == 1 => no path due to a constraint - continue - - service_paths = response['path'] - - for service_path in service_paths: - # ... "path-capacity": {"total-size": {"value": 200, "unit": 0}}, - # ... "path-latency": {"fixed-latency-characteristic": "10.000000"}, - # ... "path-cost": {"cost-name": "", "cost-value": "5.000000", "cost-algorithm": "0.000000"}, - #path_capacity = service_path['path-capacity']['total-size'] - #path_capacity_value = path_capacity['value'] - #path_capacity_unit = CapacityUnit(path_capacity['unit']) - #path_latency = service_path['path-latency']['fixed-latency-characteristic'] - #path_cost = service_path['path-cost'] - #path_cost_name = path_cost['cost-name'] - #path_cost_value = path_cost['cost-value'] - #path_cost_algorithm = path_cost['cost-algorithm'] - - path_endpoints = service_path['devices'] - for endpoint in path_endpoints: - device_uuid = endpoint['device_id'] - endpoint_uuid = endpoint['endpoint_uuid'] - endpoint_id = connection.path_hops_endpoint_ids.add() - endpoint_id.CopyFrom(endpoint_dict[device_uuid][endpoint_uuid][1]) - + # TODO: add filtering of devices and links + # TODO: add contexts, topologies, and membership of devices/links in topologies + algorithm = get_algorithm(request) + algorithm.add_devices(context_client.ListDevices(Empty())) + algorithm.add_links(context_client.ListLinks(Empty())) + algorithm.add_service_requests(request) + + #LOGGER.debug('device_list = {:s}' .format(str(algorithm.device_list ))) + #LOGGER.debug('endpoint_dict = {:s}'.format(str(algorithm.endpoint_dict))) + #LOGGER.debug('link_list = {:s}' .format(str(algorithm.link_list ))) + #LOGGER.debug('service_list = {:s}' .format(str(algorithm.service_list ))) + #LOGGER.debug('service_dict = {:s}' .format(str(algorithm.service_dict ))) + + #import time + #ts = time.time() + #algorithm.execute('request-{:f}.json'.format(ts), 'reply-{:f}.json'.format(ts)) + algorithm.execute() + + reply = algorithm.get_reply() LOGGER.info('[Compute] end ; reply = {:s}'.format(grpc_message_to_json_string(reply))) return reply diff --git a/src/pathcomp/frontend/service/algorithms/Factory.py b/src/pathcomp/frontend/service/algorithms/Factory.py new file mode 100644 index 0000000000000000000000000000000000000000..80d329a9d6d2f3adf824132aca7e4d5a35cffa99 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/Factory.py @@ -0,0 +1,33 @@ +# 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. + +from ._Algorithm import _Algorithm +from .KDisjointPathAlgorithm import KDisjointPathAlgorithm +from .KShortestPathAlgorithm import KShortestPathAlgorithm +from .ShortestPathAlgorithm import ShortestPathAlgorithm + +ALGORITHMS = { + 'shortest_path' : ShortestPathAlgorithm, + 'k_shortest_path': KShortestPathAlgorithm, + 'k_disjoint_path': KDisjointPathAlgorithm, +} + +def get_algorithm(request) -> _Algorithm: + algorithm_name = request.WhichOneof('algorithm') + algorithm_class = ALGORITHMS.get(algorithm_name) + if algorithm_class is None: + raise Exception('Algorithm({:s}) not supported'.format(str(algorithm_name))) + algorithm_settings = getattr(request, algorithm_name) + algorithm_instance = algorithm_class(algorithm_settings) + return algorithm_instance diff --git a/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..f949383c70f3f2f88a4a5559f2d535e2dbf96775 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py @@ -0,0 +1,83 @@ +# 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. + +import copy +from typing import Optional +from common.proto.pathcomp_pb2 import Algorithm_KDisjointPath, Algorithm_KShortestPath, PathCompReply +from ._Algorithm import _Algorithm +from .KShortestPathAlgorithm import KShortestPathAlgorithm + +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 + + def execute(self, dump_request_filename: Optional[str] = None, dump_reply_filename: Optional[str] = None) -> None: + algorithm = KShortestPathAlgorithm(Algorithm_KShortestPath(k_inspection=0, k_return=1)) + algorithm.sync_paths = True + algorithm.device_list = self.device_list + algorithm.device_dict = self.device_dict + algorithm.endpoint_dict = self.endpoint_dict + 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 + + disjoint_paths = dict() + + for num_path in range(self.num_disjoint): + algorithm.execute('ksp-{:d}-request.json'.format(num_path), 'ksp-{:d}-reply.txt'.format(num_path)) + response_list = algorithm.json_reply.get('response-list', []) + for response in response_list: + service_id = response['serviceId'] + service_key = (service_id['contextId'], service_id['service_uuid']) + disjoint_paths_service = disjoint_paths.setdefault(service_key, list()) + + no_path_issue = response.get('noPath', {}).get('issue') + if no_path_issue is not None: + disjoint_paths_service.append(None) + continue + + path_endpoints = response['path'][0]['devices'] + path_links = list() + path_link_ids = set() + for endpoint in path_endpoints: + device_uuid = endpoint['device_id'] + endpoint_uuid = endpoint['endpoint_uuid'] + item = algorithm.endpoint_to_link_dict.get((device_uuid, endpoint_uuid)) + if item is None: + MSG = 'Link for Endpoint({:s}, {:s}) not found' + self.logger.warning(MSG.format(device_uuid, endpoint_uuid)) + continue + json_link,_ = item + json_link_id = json_link['link_Id'] + if len(path_links) == 0 or path_links[-1]['link_Id'] != json_link_id: + path_links.append(json_link) + path_link_ids.add(json_link_id) + self.logger.info('path_links = {:s}'.format(str(path_links))) + disjoint_paths_service.append(path_links) + + new_link_list = list(filter(lambda l: l['link_Id'] not in path_link_ids, algorithm.link_list)) + self.logger.info('algorithm.link_list = {:s}'.format(str(algorithm.link_list))) + self.logger.info('new_link_list = {:s}'.format(str(new_link_list))) + algorithm.link_list = new_link_list + + + # TODO: find used links and remove them from algorithm.link_list + # TODO: compose disjoint path found + + + self.logger.info('disjoint_paths = {:s}'.format(str(disjoint_paths))) + self.json_reply = {} diff --git a/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..11da9daa59268b78768c1733333e0d2cc7882335 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/KShortestPathAlgorithm.py @@ -0,0 +1,29 @@ +# 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. + +from common.proto.pathcomp_pb2 import Algorithm_KShortestPath +from ._Algorithm import _Algorithm + +class KShortestPathAlgorithm(_Algorithm): + def __init__(self, algorithm : Algorithm_KShortestPath, class_name=__name__) -> None: + super().__init__('KSP', False, class_name=class_name) + self.k_inspection = algorithm.k_inspection + self.k_return = algorithm.k_return + + def add_service_requests(self, requested_services) -> None: + super().add_service_requests(requested_services) + for service_request in self.service_list: + service_request['algId' ] = self.algorithm_id + service_request['syncPaths'] = self.sync_paths + service_request['kPaths' ] = self.k_return diff --git a/src/pathcomp/frontend/service/algorithms/ShortestPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/ShortestPathAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..944a2d168e47f5329b4afd6103540ba76a0c5d01 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/ShortestPathAlgorithm.py @@ -0,0 +1,28 @@ +# 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. + +from common.proto.pathcomp_pb2 import Algorithm_ShortestPath +from ._Algorithm import _Algorithm + +class ShortestPathAlgorithm(_Algorithm): + def __init__(self, algorithm : Algorithm_ShortestPath, class_name=__name__) -> None: + super().__init__('SP', False, class_name=class_name) + self.k_paths = 1 + + def add_service_requests(self, requested_services) -> None: + super().add_service_requests(requested_services) + for service_request in self.service_list: + service_request['algId' ] = self.algorithm_id + service_request['syncPaths'] = self.sync_paths + service_request['kPaths' ] = self.k_paths diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..1a8b57bd03acae8705a6db6c7f828ea010d1c96c --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -0,0 +1,156 @@ +# 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. + +import json, logging, requests, uuid +from typing import Dict, List, Optional, Tuple +from common.proto.context_pb2 import Device, DeviceList, EndPointId, Link, LinkList, Service +from common.proto.pathcomp_pb2 import PathCompReply, PathCompRequest +from common.tools.grpc.Tools import grpc_message_to_json_string +from pathcomp.frontend.Config import BACKEND_URL +from .tools.ComposeRequest import compose_device, compose_link, compose_service + +class _Algorithm: + def __init__(self, algorithm_id : str, sync_paths : bool, class_name=__name__) -> None: + # algorithm_id: algorithm to be executed + # sync_paths: if multiple services are included in the request, tunes how to prevent contention. If true, + # services are computed one after the other and resources assigned to service i, are considered as + # used when computing services i+1..n; otherwise, resources are never marked as used during the + # path computation. + + self.logger = logging.getLogger(class_name) + self.algorithm_id = algorithm_id + self.sync_paths = sync_paths + + self.device_list : List[Device] = list() + self.device_dict : Dict[str, Tuple[Dict, Device]] = dict() + self.endpoint_dict : Dict[str, Dict[str, Tuple[Dict, EndPointId]]] = dict() + self.link_list : List[Link] = list() + self.link_dict : Dict[str, Tuple[Dict, Link]] = dict() + self.endpoint_to_link_dict : Dict[Tuple[str, str], Tuple[Dict, Link]] = dict() + self.service_list : List[Service] = list() + self.service_dict : Dict[Tuple[str, str], Tuple[Dict, Service]] = dict() + + def add_devices(self, grpc_devices : DeviceList) -> None: + for grpc_device in grpc_devices.devices: + json_device = compose_device(grpc_device) + self.device_list.append(json_device) + + device_uuid = json_device['device_Id'] + self.device_dict[device_uuid] = (json_device, grpc_device) + + device_endpoint_dict : Dict[str, Tuple[Dict, EndPointId]] = dict() + for json_endpoint,grpc_endpoint in zip(json_device['device_endpoints'], grpc_device.device_endpoints): + endpoint_uuid = json_endpoint['endpoint_id']['endpoint_uuid'] + endpoint_tuple = (json_endpoint['endpoint_id'], grpc_endpoint.endpoint_id) + device_endpoint_dict[endpoint_uuid] = endpoint_tuple + + self.endpoint_dict[device_uuid] = device_endpoint_dict + + def add_links(self, grpc_links : LinkList) -> None: + for grpc_link in grpc_links.links: + json_link = compose_link(grpc_link) + self.link_list.append(json_link) + + link_uuid = json_link['link_Id'] + self.link_dict[link_uuid] = (json_link, grpc_link) + + for link_endpoint_id in json_link['link_endpoint_ids']: + link_endpoint_id = link_endpoint_id['endpoint_id'] + device_uuid = link_endpoint_id['device_id'] + endpoint_uuid = link_endpoint_id['endpoint_uuid'] + endpoint_key = (device_uuid, endpoint_uuid) + link_tuple = (json_link, grpc_link) + self.endpoint_to_link_dict[endpoint_key] = link_tuple + + def add_service_requests(self, request : PathCompRequest) -> None: + for grpc_service in request.services: + json_service = compose_service(grpc_service) + self.service_list.append(json_service) + service_id = json_service['serviceId'] + service_key = (service_id['contextId'], service_id['service_uuid']) + service_tuple = (json_service, grpc_service) + self.service_dict[service_key] = service_tuple + + 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} + + 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)) + + reply = requests.post(BACKEND_URL, json=request) + self.status_code = reply.status_code + self.raw_reply = reply.content.decode('UTF-8') + + 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))) + + self.json_reply = reply.json() + + def get_reply(self) -> PathCompReply: + response_list = self.json_reply.get('response-list', []) + reply = PathCompReply() + for response in response_list: + service_id = response['serviceId'] + service_key = (service_id['contextId'], service_id['service_uuid']) + tuple_service = self.service_dict.get(service_key) + if tuple_service is None: raise Exception('ServiceKey({:s}) not found'.format(str(service_key))) + json_service, 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') + + service = reply.services.add() + service.CopyFrom(grpc_service) + + connection = reply.connections.add() + connection.connection_id.connection_uuid.uuid = str(uuid.uuid4()) + connection.service_id.CopyFrom(service.service_id) + + no_path_issue = response.get('noPath', {}).get('issue') + if no_path_issue is not None: + # no path found: leave connection with no endpoints + # no_path_issue == 1 => no path due to a constraint + continue + + service_paths = response['path'] + + for service_path in service_paths: + # ... "path-capacity": {"total-size": {"value": 200, "unit": 0}}, + # ... "path-latency": {"fixed-latency-characteristic": "10.000000"}, + # ... "path-cost": {"cost-name": "", "cost-value": "5.000000", "cost-algorithm": "0.000000"}, + #path_capacity = service_path['path-capacity']['total-size'] + #path_capacity_value = path_capacity['value'] + #path_capacity_unit = CapacityUnit(path_capacity['unit']) + #path_latency = service_path['path-latency']['fixed-latency-characteristic'] + #path_cost = service_path['path-cost'] + #path_cost_name = path_cost['cost-name'] + #path_cost_value = path_cost['cost-value'] + #path_cost_algorithm = path_cost['cost-algorithm'] + + path_endpoints = service_path['devices'] + for endpoint in path_endpoints: + device_uuid = endpoint['device_id'] + endpoint_uuid = endpoint['endpoint_uuid'] + endpoint_id = connection.path_hops_endpoint_ids.add() + endpoint_id.CopyFrom(self.endpoint_dict[device_uuid][endpoint_uuid][1]) + return reply diff --git a/src/pathcomp/frontend/service/tools/__init__.py b/src/pathcomp/frontend/service/algorithms/__init__.py similarity index 100% rename from src/pathcomp/frontend/service/tools/__init__.py rename to src/pathcomp/frontend/service/algorithms/__init__.py diff --git a/src/pathcomp/frontend/service/tools/ComposeRequest.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py similarity index 91% rename from src/pathcomp/frontend/service/tools/ComposeRequest.py rename to src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py index 68395523b4bb122483479451cc08b26094032b5c..706906b36d02d3ac5d8bcf05f4ae8d2e1243a561 100644 --- a/src/pathcomp/frontend/service/tools/ComposeRequest.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py @@ -101,7 +101,7 @@ def compose_link(grpc_link : Link) -> Dict: 'cost-characteristics': cost_characteristics, 'latency-characteristics': latency_characteristics, } -def compose_service(grpc_service : Service, algorithm : Dict) -> Dict: +def compose_service(grpc_service : Service) -> Dict: service_id = compose_service_id(grpc_service.service_id) service_type = grpc_service.service_type @@ -115,23 +115,9 @@ def compose_service(grpc_service : Service, algorithm : Dict) -> Dict: for service_constraint in grpc_service.service_constraints ] - # algorithm to be executed - algorithm_id = algorithm.get('id', 'SP') - - # if multiple services included in the request, prevent contention - # If true, services are computed one after the other and resources - # assigned to service i, are considered as used by services i+1..n - sync_paths = algorithm.get('sync', False) - - k_paths = algorithm.get('k_paths', 1) - return { 'serviceId': service_id, 'serviceType': service_type, 'service_endpoints_ids': endpoint_ids, 'service_constraints': constraints, - - 'algId': algorithm_id, - 'syncPaths': sync_paths, - 'kPaths': k_paths, } diff --git a/src/pathcomp/frontend/service/tools/Constants.py b/src/pathcomp/frontend/service/algorithms/tools/Constants.py similarity index 100% rename from src/pathcomp/frontend/service/tools/Constants.py rename to src/pathcomp/frontend/service/algorithms/tools/Constants.py diff --git a/src/pathcomp/frontend/service/algorithms/tools/__init__.py b/src/pathcomp/frontend/service/algorithms/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..70a33251242c51f49140e596b8208a19dd5245f7 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/tools/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/pathcomp/frontend/tests/Objects.py b/src/pathcomp/frontend/tests/Objects_A_B_C.py similarity index 100% rename from src/pathcomp/frontend/tests/Objects.py rename to src/pathcomp/frontend/tests/Objects_A_B_C.py diff --git a/src/pathcomp/frontend/tests/Objects_DC_CSGW_TN.py b/src/pathcomp/frontend/tests/Objects_DC_CSGW_TN.py new file mode 100644 index 0000000000000000000000000000000000000000..edb468455ba4edf5ea63a545f2b1c78d0c2cad94 --- /dev/null +++ b/src/pathcomp/frontend/tests/Objects_DC_CSGW_TN.py @@ -0,0 +1,168 @@ +# 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. + +from common.Constants import DEFAULT_CONTEXT_UUID, DEFAULT_TOPOLOGY_UUID +from common.tools.object_factory.Constraint import json_constraint +from common.tools.object_factory.Context import json_context, json_context_id +from common.tools.object_factory.Device import json_device_emulated_packet_router_disabled, json_device_id +from common.tools.object_factory.EndPoint import json_endpoints +from common.tools.object_factory.Link import get_link_uuid, json_link, json_link_id +from common.tools.object_factory.Service import get_service_uuid, json_service_l3nm_planned +from common.tools.object_factory.Topology import json_topology, json_topology_id + +def compose_device(device_uuid, endpoint_uuids, topology_id=None): + device_id = json_device_id(device_uuid) + endpoints = [(endpoint_uuid, 'copper', []) for endpoint_uuid in endpoint_uuids] + endpoints = json_endpoints(device_id, endpoints, topology_id=topology_id) + device = json_device_emulated_packet_router_disabled(device_uuid, endpoints=endpoints) + return device_id, endpoints, device + +def compose_link(endpoint_a, endpoint_z): + link_uuid = get_link_uuid(endpoint_a['endpoint_id'], endpoint_z['endpoint_id']) + link_id = json_link_id(link_uuid) + link = json_link(link_uuid, [endpoint_a['endpoint_id'], endpoint_z['endpoint_id']]) + return link_id, link + +def compose_service(endpoint_a, endpoint_z, constraints=[]): + service_uuid = get_service_uuid(endpoint_a['endpoint_id'], endpoint_z['endpoint_id']) + endpoint_ids = [endpoint_a['endpoint_id'], endpoint_z['endpoint_id']] + service = json_service_l3nm_planned(service_uuid, endpoint_ids=endpoint_ids, constraints=constraints) + return service + +# ----- Context -------------------------------------------------------------------------------------------------------- +CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_UUID) +CONTEXT = json_context(DEFAULT_CONTEXT_UUID) + +# ----- Domains -------------------------------------------------------------------------------------------------------- +# Overall network topology +TOPO_ADMIN_UUID = DEFAULT_TOPOLOGY_UUID +TOPO_ADMIN_ID = json_topology_id(TOPO_ADMIN_UUID, context_id=CONTEXT_ID) +TOPO_ADMIN = json_topology(TOPO_ADMIN_UUID, context_id=CONTEXT_ID) + +# DataCenter #1 Network +TOPO_DC1_UUID = 'DC1' +TOPO_DC1_ID = json_topology_id(TOPO_DC1_UUID, context_id=CONTEXT_ID) +TOPO_DC1 = json_topology(TOPO_DC1_UUID, context_id=CONTEXT_ID) + +# DataCenter #2 Network +TOPO_DC2_UUID = 'DC2' +TOPO_DC2_ID = json_topology_id(TOPO_DC2_UUID, context_id=CONTEXT_ID) +TOPO_DC2 = json_topology(TOPO_DC2_UUID, context_id=CONTEXT_ID) + +# CellSite #1 Network +TOPO_CS1_UUID = 'CS1' +TOPO_CS1_ID = json_topology_id(TOPO_CS1_UUID, context_id=CONTEXT_ID) +TOPO_CS1 = json_topology(TOPO_CS1_UUID, context_id=CONTEXT_ID) + +# CellSite #2 Network +TOPO_CS2_UUID = 'CS2' +TOPO_CS2_ID = json_topology_id(TOPO_CS2_UUID, context_id=CONTEXT_ID) +TOPO_CS2 = json_topology(TOPO_CS2_UUID, context_id=CONTEXT_ID) + +# Transport Network Network +TOPO_TN_UUID = 'TN' +TOPO_TN_ID = json_topology_id(TOPO_TN_UUID, context_id=CONTEXT_ID) +TOPO_TN = json_topology(TOPO_TN_UUID, context_id=CONTEXT_ID) + + +# ----- Devices -------------------------------------------------------------------------------------------------------- +# DataCenters +DEV_DC1GW_ID, DEV_DC1GW_EPS, DEV_DC1GW = compose_device('DC1-GW', ['eth1', 'eth2', 'int'], topology_id=TOPO_DC1_ID) +DEV_DC2GW_ID, DEV_DC2GW_EPS, DEV_DC2GW = compose_device('DC2-GW', ['eth1', 'eth2', 'int'], topology_id=TOPO_DC2_ID) + +# CellSites +DEV_CS1GW1_ID, DEV_CS1GW1_EPS, DEV_CS1GW1 = compose_device('CS1-GW1', ['1000', '100', '200'], topology_id=TOPO_CS1_ID) +DEV_CS1GW2_ID, DEV_CS1GW2_EPS, DEV_CS1GW2 = compose_device('CS1-GW2', ['1000', '100', '200'], topology_id=TOPO_CS1_ID) +DEV_CS2GW1_ID, DEV_CS2GW1_EPS, DEV_CS2GW1 = compose_device('CS2-GW1', ['1000', '100', '200'], topology_id=TOPO_CS2_ID) +DEV_CS2GW2_ID, DEV_CS2GW2_EPS, DEV_CS2GW2 = compose_device('CS2-GW2', ['1000', '100', '200'], topology_id=TOPO_CS2_ID) + +# Transport Network +DEV_TNR1_ID, DEV_TNR1_EPS, DEV_TNR1 = compose_device('TN-R1', ['100', '200', '1', '2', '3'], topology_id=TOPO_TN_ID) +DEV_TNR2_ID, DEV_TNR2_EPS, DEV_TNR2 = compose_device('TN-R2', ['100', '200', '1', '2', '3'], topology_id=TOPO_TN_ID) +DEV_TNR3_ID, DEV_TNR3_EPS, DEV_TNR3 = compose_device('TN-R3', ['100', '200', '1', '2', '3'], topology_id=TOPO_TN_ID) +DEV_TNR4_ID, DEV_TNR4_EPS, DEV_TNR4 = compose_device('TN-R4', ['100', '200', '1', '2', '3'], topology_id=TOPO_TN_ID) + + +# ----- Links ---------------------------------------------------------------------------------------------------------- +# InterDomain DC-CSGW +LINK_DC1GW_CS1GW1_ID, LINK_DC1GW_CS1GW1 = compose_link(DEV_DC1GW_EPS[0], DEV_CS1GW1_EPS[0]) +LINK_DC1GW_CS1GW2_ID, LINK_DC1GW_CS1GW2 = compose_link(DEV_DC1GW_EPS[1], DEV_CS1GW2_EPS[0]) +LINK_DC2GW_CS2GW1_ID, LINK_DC2GW_CS2GW1 = compose_link(DEV_DC2GW_EPS[0], DEV_CS2GW1_EPS[0]) +LINK_DC2GW_CS2GW2_ID, LINK_DC2GW_CS2GW2 = compose_link(DEV_DC2GW_EPS[1], DEV_CS2GW2_EPS[0]) + +# InterDomain CSGW-TN +LINK_CS1GW1_TNR1_ID, LINK_CS1GW1_TNR1 = compose_link(DEV_CS1GW1_EPS[1], DEV_TNR1_EPS[0]) +LINK_CS1GW2_TNR2_ID, LINK_CS1GW2_TNR2 = compose_link(DEV_CS1GW2_EPS[1], DEV_TNR2_EPS[0]) +LINK_CS1GW1_TNR2_ID, LINK_CS1GW1_TNR2 = compose_link(DEV_CS1GW1_EPS[2], DEV_TNR2_EPS[1]) +LINK_CS1GW2_TNR1_ID, LINK_CS1GW2_TNR1 = compose_link(DEV_CS1GW2_EPS[2], DEV_TNR1_EPS[1]) +LINK_CS2GW1_TNR3_ID, LINK_CS2GW1_TNR3 = compose_link(DEV_CS2GW1_EPS[1], DEV_TNR3_EPS[0]) +LINK_CS2GW2_TNR4_ID, LINK_CS2GW2_TNR4 = compose_link(DEV_CS2GW2_EPS[1], DEV_TNR4_EPS[0]) +LINK_CS2GW1_TNR4_ID, LINK_CS2GW1_TNR4 = compose_link(DEV_CS2GW1_EPS[2], DEV_TNR4_EPS[1]) +LINK_CS2GW2_TNR3_ID, LINK_CS2GW2_TNR3 = compose_link(DEV_CS2GW2_EPS[2], DEV_TNR3_EPS[1]) + +# IntraDomain TN +LINK_TNR1_TNR2_ID, LINK_TNR1_TNR2 = compose_link(DEV_TNR1_EPS[2], DEV_TNR2_EPS[3]) +LINK_TNR2_TNR3_ID, LINK_TNR2_TNR3 = compose_link(DEV_TNR2_EPS[2], DEV_TNR3_EPS[3]) +LINK_TNR3_TNR4_ID, LINK_TNR3_TNR4 = compose_link(DEV_TNR3_EPS[2], DEV_TNR4_EPS[3]) +LINK_TNR4_TNR1_ID, LINK_TNR4_TNR1 = compose_link(DEV_TNR4_EPS[2], DEV_TNR1_EPS[3]) +LINK_TNR1_TNR3_ID, LINK_TNR1_TNR3 = compose_link(DEV_TNR1_EPS[4], DEV_TNR3_EPS[4]) +LINK_TNR2_TNR4_ID, LINK_TNR2_TNR4 = compose_link(DEV_TNR2_EPS[4], DEV_TNR4_EPS[4]) + + +# ----- Service -------------------------------------------------------------------------------------------------------- +SERVICE_DC1GW_DC2GW = compose_service(DEV_DC1GW_EPS[2], DEV_DC2GW_EPS[2], constraints=[ + json_constraint('bandwidth[gbps]', 10.0), + json_constraint('latency[ms]', 12.0), +]) + +# ----- Containers ----------------------------------------------------------------------------------------------------- +CONTEXTS = [ CONTEXT] +TOPOLOGIES = [ TOPO_ADMIN, TOPO_DC1, TOPO_DC2, TOPO_CS1, TOPO_CS2, TOPO_TN] +DEVICES = [ DEV_DC1GW, DEV_DC2GW, + DEV_CS1GW1, DEV_CS1GW2, DEV_CS2GW1, DEV_CS2GW2, + DEV_TNR1, DEV_TNR2, DEV_TNR3, DEV_TNR4 ] +LINKS = [ LINK_DC1GW_CS1GW1, LINK_DC1GW_CS1GW2, LINK_DC2GW_CS2GW1, LINK_DC2GW_CS2GW2, + LINK_CS1GW1_TNR1, LINK_CS1GW2_TNR2, LINK_CS1GW1_TNR2, LINK_CS1GW2_TNR1, + LINK_CS2GW1_TNR3, LINK_CS2GW2_TNR4, LINK_CS2GW1_TNR4, LINK_CS2GW2_TNR3, + LINK_TNR1_TNR2, LINK_TNR2_TNR3, LINK_TNR3_TNR4, LINK_TNR4_TNR1, LINK_TNR1_TNR3, LINK_TNR2_TNR4 ] +SERVICES = [ SERVICE_DC1GW_DC2GW ] + +OBJECTS_PER_TOPOLOGY = [ + (TOPO_ADMIN_ID, + [ DEV_DC1GW_ID, DEV_DC2GW_ID, + DEV_CS1GW1_ID, DEV_CS1GW2_ID, DEV_CS2GW1_ID, DEV_CS2GW2_ID, + DEV_TNR1_ID, DEV_TNR2_ID, DEV_TNR3_ID, DEV_TNR4_ID ], + [ LINK_DC1GW_CS1GW1_ID, LINK_DC1GW_CS1GW2_ID, LINK_DC2GW_CS2GW1_ID, LINK_DC2GW_CS2GW2_ID, + LINK_CS1GW1_TNR1_ID, LINK_CS1GW2_TNR2_ID, LINK_CS1GW1_TNR2_ID, LINK_CS1GW2_TNR1_ID, + LINK_CS2GW1_TNR3_ID, LINK_CS2GW2_TNR4_ID, LINK_CS2GW1_TNR4_ID, LINK_CS2GW2_TNR3_ID, + LINK_TNR1_TNR2_ID, LINK_TNR2_TNR3_ID, LINK_TNR3_TNR4_ID, LINK_TNR4_TNR1_ID, LINK_TNR1_TNR3_ID, + LINK_TNR2_TNR4_ID ], + ), + (TOPO_DC1_ID, + [DEV_DC1GW_ID], + []), + (TOPO_DC2_ID, + [DEV_DC2GW_ID], + []), + (TOPO_CS1_ID, + [DEV_CS1GW1_ID, DEV_CS1GW2_ID], + []), + (TOPO_CS2_ID, + [DEV_CS2GW1_ID, DEV_CS2GW2_ID], + []), + (TOPO_TN_ID, + [DEV_TNR1_ID, DEV_TNR2_ID, DEV_TNR3_ID, DEV_TNR4_ID], + [LINK_TNR1_TNR2_ID, LINK_TNR2_TNR3_ID, LINK_TNR3_TNR4_ID, LINK_TNR4_TNR1_ID, LINK_TNR1_TNR3_ID, + LINK_TNR2_TNR4_ID]), +] diff --git a/src/pathcomp/frontend/tests/PrepareTestScenario.py b/src/pathcomp/frontend/tests/PrepareTestScenario.py index 9fb57e41b51f99b52bf71f7c6fc4d02d98ea100e..cb0d8e466ca418226ff96f77a1cfaffc9dbdb6af 100644 --- a/src/pathcomp/frontend/tests/PrepareTestScenario.py +++ b/src/pathcomp/frontend/tests/PrepareTestScenario.py @@ -24,7 +24,7 @@ from pathcomp.frontend.tests.MockService_Dependencies import MockService_Depende LOCAL_HOST = '127.0.0.1' MOCKSERVICE_PORT = 10000 -PATHCOMP_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_grpc(ServiceNameEnum.PATHCOMP) # avoid privileged ports +PATHCOMP_SERVICE_PORT = MOCKSERVICE_PORT + int(get_service_port_grpc(ServiceNameEnum.PATHCOMP)) # avoid privileged ports os.environ[get_env_var_name(ServiceNameEnum.PATHCOMP, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) os.environ[get_env_var_name(ServiceNameEnum.PATHCOMP, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(PATHCOMP_SERVICE_PORT) diff --git a/src/pathcomp/frontend/tests/test_unitary.py b/src/pathcomp/frontend/tests/test_unitary.py index 7f614e69c2c6594c2fbf7f1b85b5ae7a9f71f328..f452a5b6563662d4bac31d0206d55db7a9ee2096 100644 --- a/src/pathcomp/frontend/tests/test_unitary.py +++ b/src/pathcomp/frontend/tests/test_unitary.py @@ -12,14 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging +import copy, logging, os from common.proto.context_pb2 import Context, ContextId, DeviceId, Link, LinkId, Topology, Device, TopologyId from common.proto.pathcomp_pb2 import PathCompRequest from common.tools.grpc.Tools import grpc_message_to_json +from common.tools.object_factory.Constraint import json_constraint from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from pathcomp.frontend.client.PathCompClient import PathCompClient -from .Objects import CONTEXTS, DEVICES, LINKS, OBJECTS_PER_TOPOLOGY, SERVICES, TOPOLOGIES + +# Scenarios: +#from .Objects_A_B_C import CONTEXTS, DEVICES, LINKS, OBJECTS_PER_TOPOLOGY, SERVICES, TOPOLOGIES +from .Objects_DC_CSGW_TN import CONTEXTS, DEVICES, LINKS, OBJECTS_PER_TOPOLOGY, SERVICES, TOPOLOGIES + +# configure backend environment variables before overwriting them with fixtures to use real backend pathcomp +DEFAULT_PATHCOMP_BACKEND_SCHEME = 'http' +DEFAULT_PATHCOMP_BACKEND_HOST = '127.0.0.1' +DEFAULT_PATHCOMP_BACKEND_PORT = '8081' +DEFAULT_PATHCOMP_BACKEND_BASEURL = '/pathComp/api/v1/compRoute' + +os.environ['PATHCOMP_BACKEND_SCHEME'] = os.environ.get('PATHCOMP_BACKEND_SCHEME', DEFAULT_PATHCOMP_BACKEND_SCHEME) +os.environ['PATHCOMP_BACKEND_HOST'] = os.environ.get('PATHCOMPSERVICE_SERVICE_HOST', DEFAULT_PATHCOMP_BACKEND_HOST) +os.environ['PATHCOMP_BACKEND_PORT'] = os.environ.get('PATHCOMPSERVICE_SERVICE_PORT_HTTP', DEFAULT_PATHCOMP_BACKEND_PORT) +os.environ['PATHCOMP_BACKEND_BASEURL'] = os.environ.get('PATHCOMP_BACKEND_BASEURL', DEFAULT_PATHCOMP_BACKEND_BASEURL) + from .PrepareTestScenario import ( # pylint: disable=unused-import # be careful, order of symbols is important here! mock_service, pathcomp_service, context_client, device_client, pathcomp_client) @@ -27,7 +43,6 @@ from .PrepareTestScenario import ( # pylint: disable=unused-import LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) - def test_prepare_environment( context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient): # pylint: disable=redefined-outer-name @@ -55,7 +70,61 @@ def test_prepare_environment( context_client.SetTopology(topology) -def test_request_service( +def test_request_service_shortestpath( + pathcomp_client : PathCompClient): # pylint: disable=redefined-outer-name + + request_services = copy.deepcopy(SERVICES) + request_services[0]['service_constraints'] = [ + json_constraint('bandwidth[gbps]', 1000.0), + json_constraint('latency[ms]', 1200.0), + ] + pathcomp_request = PathCompRequest(services=request_services) + pathcomp_request.shortest_path.Clear() # hack to select the shortest path algorithm that has no attributes + + pathcomp_reply = pathcomp_client.Compute(pathcomp_request) + + pathcomp_reply = grpc_message_to_json(pathcomp_reply) + reply_services = pathcomp_reply['services'] + reply_connections = pathcomp_reply['connections'] + assert len(request_services) <= len(reply_services) + request_service_ids = { + '{:s}/{:s}'.format( + svc['service_id']['context_id']['context_uuid']['uuid'], + svc['service_id']['service_uuid']['uuid'] + ) + for svc in request_services + } + reply_service_ids = { + '{:s}/{:s}'.format( + svc['service_id']['context_id']['context_uuid']['uuid'], + svc['service_id']['service_uuid']['uuid'] + ) + for svc in reply_services + } + # Assert all requested services have a reply + # It permits having other services not requested (i.e., sub-services) + assert len(request_service_ids.difference(reply_service_ids)) == 0 + + reply_connection_service_ids = { + '{:s}/{:s}'.format( + conn['service_id']['context_id']['context_uuid']['uuid'], + conn['service_id']['service_uuid']['uuid'] + ) + for conn in reply_connections + } + # Assert all requested services have a connection associated + # It permits having other connections not requested (i.e., connections for sub-services) + assert len(request_service_ids.difference(reply_connection_service_ids)) == 0 + + # TODO: implement other checks. examples: + # - request service and reply service endpoints match + # - request service and reply connection endpoints match + # - reply sub-service and reply sub-connection endpoints match + # - others? + #for json_service,json_connection in zip(json_services, json_connections): + + +def test_request_service_kshortestpath( pathcomp_client : PathCompClient): # pylint: disable=redefined-outer-name request_services = SERVICES @@ -106,6 +175,56 @@ def test_request_service( #for json_service,json_connection in zip(json_services, json_connections): +def test_request_service_kdisjointpath( + pathcomp_client : PathCompClient): # pylint: disable=redefined-outer-name + + request_services = SERVICES + pathcomp_request = PathCompRequest(services=request_services) + pathcomp_request.k_disjoint_path.num_disjoint = 2 #pylint: disable=no-member + + pathcomp_reply = pathcomp_client.Compute(pathcomp_request) + + pathcomp_reply = grpc_message_to_json(pathcomp_reply) + reply_services = pathcomp_reply['services'] + reply_connections = pathcomp_reply['connections'] + assert len(request_services) <= len(reply_services) + request_service_ids = { + '{:s}/{:s}'.format( + svc['service_id']['context_id']['context_uuid']['uuid'], + svc['service_id']['service_uuid']['uuid'] + ) + for svc in request_services + } + reply_service_ids = { + '{:s}/{:s}'.format( + svc['service_id']['context_id']['context_uuid']['uuid'], + svc['service_id']['service_uuid']['uuid'] + ) + for svc in reply_services + } + # Assert all requested services have a reply + # It permits having other services not requested (i.e., sub-services) + assert len(request_service_ids.difference(reply_service_ids)) == 0 + + reply_connection_service_ids = { + '{:s}/{:s}'.format( + conn['service_id']['context_id']['context_uuid']['uuid'], + conn['service_id']['service_uuid']['uuid'] + ) + for conn in reply_connections + } + # Assert all requested services have a connection associated + # It permits having other connections not requested (i.e., connections for sub-services) + assert len(request_service_ids.difference(reply_connection_service_ids)) == 0 + + # TODO: implement other checks. examples: + # - request service and reply service endpoints match + # - request service and reply connection endpoints match + # - reply sub-service and reply sub-connection endpoints match + # - others? + #for json_service,json_connection in zip(json_services, json_connections): + + def test_cleanup_environment( context_client : ContextClient, # pylint: disable=redefined-outer-name device_client : DeviceClient): # pylint: disable=redefined-outer-name