diff --git a/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py index f949383c70f3f2f88a4a5559f2d535e2dbf96775..1e676b0aec93d083217bd53ca8e078b00ddf8376 100644 --- a/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py +++ b/src/pathcomp/frontend/service/algorithms/KDisjointPathAlgorithm.py @@ -13,7 +13,8 @@ # limitations under the License. import copy -from typing import Optional +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 ._Algorithm import _Algorithm from .KShortestPathAlgorithm import KShortestPathAlgorithm @@ -23,6 +24,36 @@ class KDisjointPathAlgorithm(_Algorithm): super().__init__('KDP', False, class_name=class_name) self.num_disjoint = algorithm.num_disjoint + def get_link_from_endpoint(self, endpoint : Dict) -> Tuple[Dict, Link]: + device_uuid = endpoint['device_id'] + endpoint_uuid = endpoint['endpoint_uuid'] + item = self.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)) + return None + return item + + def path_to_links(self, path_endpoints : List[Dict]) -> Tuple[List[Dict], Set[str]]: + path_links = list() + path_link_ids = set() + for endpoint in path_endpoints: + link_tuple = self.get_link_from_endpoint(endpoint) + if link_tuple is None: continue + json_link,_ = link_tuple + 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) + return path_links, path_link_ids + + 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))) + return new_link_list + 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 @@ -35,49 +66,38 @@ class KDisjointPathAlgorithm(_Algorithm): algorithm.service_list = self.service_list algorithm.service_dict = self.service_dict - disjoint_paths = 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): - algorithm.execute('ksp-{:d}-request.json'.format(num_path), 'ksp-{:d}-reply.txt'.format(num_path)) + #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() 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()) + json_reply_service = self.json_reply.setdefault(service_key, list()) no_path_issue = response.get('noPath', {}).get('issue') if no_path_issue is not None: - disjoint_paths_service.append(None) + json_reply_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 + 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.info('disjoint_paths = {:s}'.format(str(disjoint_paths))) - self.json_reply = {} + def get_reply(self) -> PathCompReply: + reply = PathCompReply() + for service_key,paths in self.json_reply.items(): + grpc_service = self.add_service_to_reply(reply, service_key[0], service_key[1]) + for path_endpoints in paths: + if path_endpoints is None: continue + grpc_connection = self.add_connection_to_reply(reply, grpc_service) + self.add_path_to_connection(grpc_connection, path_endpoints) + return reply diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 1a8b57bd03acae8705a6db6c7f828ea010d1c96c..d4973f168dd7bce9fb830600c2d010fc238f3b48 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -14,7 +14,7 @@ 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.context_pb2 import Connection, 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 @@ -32,13 +32,13 @@ class _Algorithm: self.algorithm_id = algorithm_id self.sync_paths = sync_paths - self.device_list : List[Device] = list() + self.device_list : List[Dict] = 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_list : List[Dict] = 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_list : List[Dict] = list() self.service_dict : Dict[Tuple[str, str], Tuple[Dict, Service]] = dict() def add_devices(self, grpc_devices : DeviceList) -> None: @@ -105,26 +105,40 @@ class _Algorithm: self.json_reply = reply.json() + def add_path_to_connection(self, connection : Connection, path_endpoints : List[Dict]) -> None: + 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]) + + def add_connection_to_reply(self, reply : PathCompReply, service : Service) -> Connection: + connection = reply.connections.add() + connection.connection_id.connection_uuid.uuid = str(uuid.uuid4()) + connection.service_id.CopyFrom(service.service_id) + return connection + + def add_service_to_reply(self, reply : PathCompReply, context_uuid : str, service_uuid : str) -> Service: + service_key = (context_uuid, 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))) + _, 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) + + return grpc_service + 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) + grpc_service = self.add_service_to_reply(reply, service_id['contextId'], service_id['service_uuid']) no_path_issue = response.get('noPath', {}).get('issue') if no_path_issue is not None: @@ -132,9 +146,10 @@ class _Algorithm: # no_path_issue == 1 => no path due to a constraint continue - service_paths = response['path'] + for service_path in response['path']: + grpc_connection = self.add_connection_to_reply(reply, grpc_service) + self.add_path_to_connection(grpc_connection, service_path['devices']) - 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"}, @@ -147,10 +162,4 @@ class _Algorithm: #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/misc/example-results-kdisjointpaths.json b/src/pathcomp/misc/example-results-kdisjointpaths.json new file mode 100644 index 0000000000000000000000000000000000000000..9eda25d484e45db53471ea3f655d511cbcc42c18 --- /dev/null +++ b/src/pathcomp/misc/example-results-kdisjointpaths.json @@ -0,0 +1,73 @@ +{ + "connections": [ + { + "connection_id": {"connection_uuid": {"uuid": "d8cb463a-77c4-4149-80fc-32c4842a191d"}}, + "path_hops_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "DC1-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC1"}}}, + {"device_id": {"device_uuid": {"uuid": "DC1-GW"}}, "endpoint_uuid": {"uuid": "eth1"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC1"}}}, + {"device_id": {"device_uuid": {"uuid": "CS1-GW1"}}, "endpoint_uuid": {"uuid": "200"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "CS1"}}}, + {"device_id": {"device_uuid": {"uuid": "TN-R2"}}, "endpoint_uuid": {"uuid": "1"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "TN"}}}, + {"device_id": {"device_uuid": {"uuid": "TN-R3"}}, "endpoint_uuid": {"uuid": "100"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "TN"}}}, + {"device_id": {"device_uuid": {"uuid": "CS2-GW1"}}, "endpoint_uuid": {"uuid": "1000"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "CS2"}}}, + {"device_id": {"device_uuid": {"uuid": "DC2-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC2"}}} + ], + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "svc:DC1-GW/int==DC2-GW/int"} + }, + "sub_service_ids": [] + }, + { + "connection_id": {"connection_uuid": {"uuid": "9f7c5075-0736-4d2a-8435-b8e8c37fa6ea"}}, + "path_hops_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "DC1-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC1"}}}, + {"device_id": {"device_uuid": {"uuid": "DC1-GW"}}, "endpoint_uuid": {"uuid": "eth2"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC1"}}}, + {"device_id": {"device_uuid": {"uuid": "CS1-GW2"}}, "endpoint_uuid": {"uuid": "200"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "CS1"}}}, + {"device_id": {"device_uuid": {"uuid": "TN-R1"}}, "endpoint_uuid": {"uuid": "3"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "TN"}}}, + {"device_id": {"device_uuid": {"uuid": "TN-R3"}}, "endpoint_uuid": {"uuid": "200"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "TN"}}}, + {"device_id": {"device_uuid": {"uuid": "CS2-GW2"}}, "endpoint_uuid": {"uuid": "1000"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "CS2"}}}, + {"device_id": {"device_uuid": {"uuid": "DC2-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC2"}}} + ], + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "svc:DC1-GW/int==DC2-GW/int"} + }, + "sub_service_ids": [] + } + ], + "services": [ + { + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "svc:DC1-GW/int==DC2-GW/int"} + }, + "service_type": "SERVICETYPE_L3NM", + "service_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "DC1-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC1"}}}, + {"device_id": {"device_uuid": {"uuid": "DC2-GW"}}, "endpoint_uuid": {"uuid": "int"}, + "topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "DC2"}}} + ], + "service_status": {"service_status": "SERVICESTATUS_PLANNED"}, + "service_constraints": [ + {"custom": {"constraint_type": "bandwidth[gbps]", "constraint_value": "10.0"}}, + {"custom": {"constraint_type": "latency[ms]", "constraint_value": "12.0"}} + ], + "service_config": {"config_rules": []} + } + ] +} \ No newline at end of file diff --git a/src/pathcomp/test-commands.sh b/src/pathcomp/misc/test-commands.sh similarity index 96% rename from src/pathcomp/test-commands.sh rename to src/pathcomp/misc/test-commands.sh index cb764fae11bf1326edce75e24cc9229c0f480379..17cf092e8cff5506d56784af092aaab8da7fec94 100644 --- a/src/pathcomp/test-commands.sh +++ b/src/pathcomp/misc/test-commands.sh @@ -29,3 +29,5 @@ docker network rm teraflowbridge docker images --filter="dangling=true" --quiet | xargs -r docker rmi docker exec -i pathcomp bash -c "pytest --log-level=INFO --verbose pathcomp/tests/test_unitary.py" + +./scripts/run_tests_locally-pathcomp-frontend.sh