diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 0286e1420a6c96a140d39380a2fa35d24f991abf..6cc4c5496597cce06be04587d69335d0a614cef5 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -25,6 +25,10 @@ from .tools.ComposeRequest import compose_device, compose_link, compose_service from .tools.ComputeSubServices import ( convert_explicit_path_hops_to_connections, convert_explicit_path_hops_to_plain_connection) +SRC_END = 'src' +DST_END = 'dst' +SENSE = [SRC_END, DST_END] + class _Algorithm: def __init__(self, algorithm_id : str, sync_paths : bool, class_name=__name__) -> None: # algorithm_id: algorithm to be executed @@ -44,7 +48,7 @@ class _Algorithm: self.endpoint_name_mapping : Dict[Tuple[str, str], str] = dict() 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.endpoint_to_link_dict : Dict[Tuple[str, str, str], Tuple[Dict, Link]] = dict() self.service_list : List[Dict] = list() self.service_dict : Dict[Tuple[str, str], Tuple[Dict, Service]] = dict() @@ -86,11 +90,11 @@ class _Algorithm: 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']: + for i,link_endpoint_id in enumerate(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) + endpoint_key = (device_uuid, endpoint_uuid, SENSE[i]) link_tuple = (json_link, grpc_link) self.endpoint_to_link_dict[endpoint_key] = link_tuple @@ -175,7 +179,9 @@ class _Algorithm: MSG = 'Unhandled generic Config Rules for service {:s} {:s}' self.logger.warning(MSG.format(str(service_uuid), str(ServiceTypeEnum.Name(service_type)))) - compose_device_config_rules(config_rules, service.service_config.config_rules, path_hops) + compose_device_config_rules( + config_rules, service.service_config.config_rules, path_hops, + self.device_name_mapping, self.endpoint_name_mapping) if path_hops is not None and len(path_hops) > 0: ingress_endpoint_id = service.service_endpoint_ids.add() @@ -214,7 +220,7 @@ class _Algorithm: 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 - grpc_services[service_key] = grpc_orig_service + grpc_services[orig_service_key] = grpc_orig_service continue orig_config_rules = grpc_orig_service.service_config.config_rules diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 30845bb11f2b027d24da3e42e4e4fee12b7da1ba..91367e23f29a02aa3e9605fcd0d2864b9191d800 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, re -from typing import Dict, List, Optional +import itertools, json, re +from typing import Dict, List, Optional, Tuple from common.proto.context_pb2 import ConfigRule from common.tools.object_factory.ConfigRule import json_config_rule_set @@ -71,7 +71,11 @@ def compose_l3nm_config_rules(main_service_config_rules : List, subservice_confi def compose_tapi_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: compose_config_rules(main_service_config_rules, subservice_config_rules, TAPI_SETTINGS_FIELD_DEFAULTS) -def compose_device_config_rules(config_rules : List, subservice_config_rules : List, path_hops : List) -> None: +def compose_device_config_rules( + config_rules : List, subservice_config_rules : List, path_hops : List, + device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] +) -> None: + endpoints_traversed = set() for path_hop in path_hops: device_uuid_or_name = path_hop['device'] @@ -82,8 +86,16 @@ def compose_device_config_rules(config_rules : List, subservice_config_rules : L if config_rule.WhichOneof('config_rule') != 'custom': continue match = DEV_EP_SETTINGS.match(config_rule.custom.resource_key) if match is None: continue + device_uuid_or_name = match.group(1) + device_name_or_uuid = device_name_mapping[device_uuid_or_name] + device_keys = {device_uuid_or_name, device_name_or_uuid} + endpoint_uuid_or_name = match.group(2) - dev_ep_kep = (device_uuid_or_name, endpoint_uuid_or_name) - if dev_ep_kep not in endpoints_traversed: continue + endpoint_name_or_uuid_1 = endpoint_name_mapping[(device_uuid_or_name, endpoint_uuid_or_name)] + endpoint_name_or_uuid_2 = endpoint_name_mapping[(device_name_or_uuid, endpoint_uuid_or_name)] + endpoint_keys = {endpoint_uuid_or_name, endpoint_name_or_uuid_1, endpoint_name_or_uuid_2} + + device_endpoint_keys = set(itertools.product(device_keys, endpoint_keys)) + if len(device_endpoint_keys.intersection(endpoints_traversed)) == 0: continue subservice_config_rules.append(config_rule) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py index ee85f0bb083500c655e78798bbcd2bd00e8a4501..e2c6dc13804703d89242b27156763ce887aa4884 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeRequest.py @@ -118,11 +118,11 @@ def compose_link(grpc_link : Link) -> Dict: for link_endpoint_id in grpc_link.link_endpoint_ids ] - forwarding_direction = LinkForwardingDirection.BIDIRECTIONAL.value + forwarding_direction = LinkForwardingDirection.UNIDIRECTIONAL.value total_potential_capacity = compose_capacity(200, CapacityUnit.MBPS.value) available_capacity = compose_capacity(200, CapacityUnit.MBPS.value) cost_characteristics = compose_cost_characteristics('linkcost', '1', '0') - latency_characteristics = compose_latency_characteristics('2') + latency_characteristics = compose_latency_characteristics('1') return { 'link_Id': link_uuid, 'link_endpoint_ids': endpoint_ids, 'forwarding_direction': forwarding_direction, diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py index 75701b99e327792f0e97068c25b594976e1ebc9e..1f2b4df9ca91f8588d947608896316da753dd0be 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py @@ -30,145 +30,41 @@ # ] # # connections=[ -# (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), ServiceTypeEnum.TAPI, [ +# (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, [ # {'device': 'TN-OLS', 'ingress_ep': '833760219d0f', 'egress_ep': 'cf176771a4b9'} # ], []), -# (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), ServiceTypeEnum.L2NM, [ +# (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), ServiceTypeEnum.SERVICETYPE_L2NM, [ # {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'}, # {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'}, # {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'}, # {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'} # ], [UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e')]), -# (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), ServiceTypeEnum.L2NM, [ +# (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), ServiceTypeEnum.SERVICETYPE_L2NM, [ # {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'}, # {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'} # ], [UUID('c2e57966-5d82-4705-a5fe-44cf6487219e')]) # ] -import enum, json, queue, uuid +import logging, queue, uuid from typing import Dict, List, Optional, Tuple from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import Device, ServiceTypeEnum +from .ResourceGroups import IGNORED_DEVICE_TYPES, get_resource_classification +from .ServiceTypes import get_service_type -class StackActionEnum(enum.Enum): - PATH_INGRESS = 'ingress' - CREATE_CONNECTION = 'create' - APPEND_PATH_HOP = 'append' - CHAIN_CONNECTION = 'chain' - TERMINATE_CONNECTION = 'terminate' - -def is_datacenter(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.DATACENTER, DeviceTypeEnum.EMULATED_DATACENTER} - -def is_packet_router(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER} - -def is_packet_switch(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH} - -def is_packet_device(dev_type : Optional[DeviceTypeEnum]) -> bool: - return is_packet_router(dev_type) or is_packet_switch(dev_type) - -def is_tfs_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.TERAFLOWSDN_CONTROLLER} - -def is_mw_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM} - -def is_ipm_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.XR_CONSTELLATION, DeviceTypeEnum.EMULATED_XR_CONSTELLATION} - -def is_ols_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.OPEN_LINE_SYSTEM, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM} - -def is_subdevice(dev_manager : Optional[str]) -> bool: - return dev_manager is not None - -def is_subdevice_equal(dev_manager_a : Optional[str], dev_manager_b : Optional[str]) -> bool: - if dev_manager_a is None and dev_manager_b is None: return True - if dev_manager_a is not None and dev_manager_b is not None: return dev_manager_a == dev_manager_b - return False - -def get_action( - prv_type : Optional[DeviceTypeEnum], prv_manager : Optional[str], - cur_type : DeviceTypeEnum, cur_manager : Optional[str] -) -> StackActionEnum: - if prv_type is None: - return StackActionEnum.PATH_INGRESS - - if is_datacenter(prv_type): - if is_packet_device(cur_type): return StackActionEnum.CREATE_CONNECTION - if is_tfs_controller(cur_type): return StackActionEnum.CREATE_CONNECTION - - if is_packet_device(prv_type): - if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_packet_device(cur_type): - if is_subdevice_equal(cur_manager, prv_manager): return StackActionEnum.APPEND_PATH_HOP - if is_subdevice(prv_manager) and not is_subdevice(cur_manager): return StackActionEnum.TERMINATE_CONNECTION - if not is_subdevice(prv_manager) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - - if is_mw_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - if is_ols_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - if is_tfs_controller(cur_type) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - - if is_mw_controller(prv_type) or is_ols_controller(prv_type): - if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION - - if is_tfs_controller(prv_type): - if is_tfs_controller(cur_type) and is_subdevice_equal(prv_manager, cur_manager): return StackActionEnum.APPEND_PATH_HOP - if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_mw_controller(cur_type) or is_ols_controller(cur_type): return StackActionEnum.CHAIN_CONNECTION - - str_fields = ', '.join([ - 'prv_type={:s}'.format(str(prv_type)), 'prv_manager={:s}'.format(str(prv_manager)), - 'cur_type={:s}'.format(str(cur_type)), 'cur_manager={:s}'.format(str(cur_manager)), - ]) - raise Exception('Undefined Action for ({:s})'.format(str_fields)) - -def get_device_manager_uuid(device : Device) -> Optional[str]: - for config_rule in device.device_config.config_rules: - if config_rule.WhichOneof('config_rule') != 'custom': continue - if config_rule.custom.resource_key != '_manager': continue - device_manager_id = json.loads(config_rule.custom.resource_value) - return device_manager_id['uuid'] - return None - -def get_device_type( - grpc_device : Device, device_dict : Dict[str, Tuple[Dict, Device]], device_manager_uuid : Optional[str] -) -> DeviceTypeEnum: - if device_manager_uuid is None: - return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member - device_manager_tuple = device_dict.get(device_manager_uuid) - if device_manager_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_manager_uuid))) - _,grpc_device = device_manager_tuple - return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member - -SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM} - -def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum: - if is_tfs_controller(device_type) or is_packet_router(device_type): - if prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type - if is_packet_switch(device_type) or is_mw_controller(device_type): - if prv_service_type == ServiceTypeEnum.SERVICETYPE_L2NM: return prv_service_type - if is_ols_controller(device_type) or is_ipm_controller(device_type): - return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE - - str_fields = ', '.join([ - 'device_type={:s}'.format(str(device_type)), - ]) - raise Exception('Undefined Service Type for ({:s})'.format(str_fields)) +LOGGER = logging.getLogger(__name__) def convert_explicit_path_hops_to_connections( path_hops : List[Dict], device_dict : Dict[str, Tuple[Dict, Device]], main_service_uuid : str, main_service_type : ServiceTypeEnum ) -> List[Tuple[str, int, List[str], List[str]]]: + LOGGER.debug('path_hops={:s}'.format(str(path_hops))) + connection_stack = queue.LifoQueue() connections : List[Tuple[str, int, List[str], List[str]]] = list() prv_device_uuid = None - prv_device_type = None - prv_manager_uuid = None + prv_res_class : Tuple[Optional[int], Optional[DeviceTypeEnum], Optional[str]] = None, None, None for path_hop in path_hops: device_uuid = path_hop['device'] @@ -177,29 +73,35 @@ def convert_explicit_path_hops_to_connections( if device_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_uuid))) _,grpc_device = device_tuple - manager_uuid = get_device_manager_uuid(grpc_device) - device_type = get_device_type(grpc_device, device_dict, manager_uuid) - action = get_action(prv_device_type, prv_manager_uuid, device_type, manager_uuid) + res_class = get_resource_classification(grpc_device, device_dict) + if res_class[1] in IGNORED_DEVICE_TYPES: continue - if action == StackActionEnum.PATH_INGRESS: + if prv_res_class[0] is None: + # path ingress connection_stack.put((main_service_uuid, main_service_type, [path_hop], [])) - elif action == StackActionEnum.CREATE_CONNECTION: - connection_uuid = str(uuid.uuid4()) - prv_service_type = connection_stack.queue[-1][1] - service_type = get_service_type(device_type, prv_service_type) - connection_stack.put((connection_uuid, service_type, [path_hop], [])) - elif action == StackActionEnum.APPEND_PATH_HOP: - connection_stack.queue[-1][2].append(path_hop) - elif action == StackActionEnum.CHAIN_CONNECTION: - connection = connection_stack.get() - connections.append(connection) - connection_stack.queue[-1][3].append(connection[0]) - + elif prv_res_class[0] > res_class[0]: + # create underlying connection connection_uuid = str(uuid.uuid4()) prv_service_type = connection_stack.queue[-1][1] - service_type = get_service_type(device_type, prv_service_type) + service_type = get_service_type(res_class[1], prv_service_type) connection_stack.put((connection_uuid, service_type, [path_hop], [])) - elif action == StackActionEnum.TERMINATE_CONNECTION: + elif prv_res_class[0] == res_class[0]: + # same resource group kind + if prv_res_class[1] == res_class[1] and prv_res_class[2] == res_class[2]: + # same device type and device manager: connection continues + connection_stack.queue[-1][2].append(path_hop) + else: + # different device type or device manager: chain connections + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + + connection_uuid = str(uuid.uuid4()) + prv_service_type = connection_stack.queue[-1][1] + service_type = get_service_type(res_class[1], prv_service_type) + connection_stack.put((connection_uuid, service_type, [path_hop], [])) + elif prv_res_class[0] < res_class[0]: + # underlying connection ended connection = connection_stack.get() connections.append(connection) connection_stack.queue[-1][3].append(connection[0]) @@ -208,11 +110,11 @@ def convert_explicit_path_hops_to_connections( raise Exception('Uncontrolled condition') prv_device_uuid = device_uuid - prv_device_type = device_type - prv_manager_uuid = manager_uuid + prv_res_class = res_class # path egress connections.append(connection_stack.get()) + LOGGER.debug('connections={:s}'.format(str(connections))) assert connection_stack.empty() return connections diff --git a/src/pathcomp/frontend/service/algorithms/tools/EroPathToHops.py b/src/pathcomp/frontend/service/algorithms/tools/EroPathToHops.py index c8a902999ddfb5011fd7ec09fa99ff6fa697ea40..670757d76b7d21ecf28f6ead4e8bc4e21951d18e 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/EroPathToHops.py +++ b/src/pathcomp/frontend/service/algorithms/tools/EroPathToHops.py @@ -43,13 +43,17 @@ # import logging -from typing import Dict, List +from typing import Dict, List, Tuple +from common.proto.context_pb2 import Link LOGGER = logging.getLogger(__name__) -def eropath_to_hops(ero_path : List[Dict], endpoint_to_link_dict : Dict) -> List[Dict]: +def eropath_to_hops( + ero_path : List[Dict], endpoint_to_link_dict : Dict[Tuple[str, str, str], Tuple[Dict, Link]] +) -> List[Dict]: try: path_hops = [] + num_ero_hops = len(ero_path) for endpoint in ero_path: device_uuid = endpoint['device_id'] endpoint_uuid = endpoint['endpoint_uuid'] @@ -59,23 +63,17 @@ def eropath_to_hops(ero_path : List[Dict], endpoint_to_link_dict : Dict) -> List continue last_hop = path_hops[-1] - if (last_hop['device'] == device_uuid): - if ('ingress_ep' not in last_hop) or ('egress_ep' in last_hop): continue - last_hop['egress_ep'] = endpoint_uuid - continue + if last_hop['device'] != device_uuid: raise Exception('Malformed path') + last_hop['egress_ep'] = endpoint_uuid + + if num_ero_hops - 1 == len(path_hops): break - endpoint_key = (last_hop['device'], last_hop['egress_ep']) - link_tuple = endpoint_to_link_dict.get(endpoint_key) - ingress = next(iter([ - ep_id for ep_id in link_tuple[0]['link_endpoint_ids'] - if (ep_id['endpoint_id']['device_id'] == device_uuid) and\ - (ep_id['endpoint_id']['endpoint_uuid'] != endpoint_uuid) - ]), None) - if ingress['endpoint_id']['device_id'] != device_uuid: raise Exception('Malformed path') + link_tuple = endpoint_to_link_dict[(device_uuid, endpoint_uuid, 'src')] + if link_tuple is None: raise Exception('Malformed path') + ingress = link_tuple[0]['link_endpoint_ids'][-1] path_hops.append({ 'device': ingress['endpoint_id']['device_id'], - 'ingress_ep': ingress['endpoint_id']['endpoint_uuid'], - 'egress_ep': endpoint_uuid, + 'ingress_ep': ingress['endpoint_id']['endpoint_uuid'] }) return path_hops except: diff --git a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py new file mode 100644 index 0000000000000000000000000000000000000000..0f6ee63dc14352a5af36cbac84929c5b4381a074 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py @@ -0,0 +1,94 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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 json +from typing import Dict, Optional, Tuple +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import Device +from common.tools.grpc.Tools import grpc_message_to_json_string + +DEVICE_TYPE_TO_DEEPNESS = { + DeviceTypeEnum.EMULATED_DATACENTER.value : 90, + DeviceTypeEnum.DATACENTER.value : 90, + DeviceTypeEnum.NETWORK.value : 90, + + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value : 70, + DeviceTypeEnum.PACKET_ROUTER.value : 70, + + DeviceTypeEnum.EMULATED_PACKET_SWITCH.value : 60, + DeviceTypeEnum.PACKET_SWITCH.value : 60, + DeviceTypeEnum.EMULATED_P4_SWITCH.value : 60, + DeviceTypeEnum.P4_SWITCH.value : 60, + + DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value : 40, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value : 40, + + DeviceTypeEnum.EMULATED_XR_CONSTELLATION.value : 40, + DeviceTypeEnum.XR_CONSTELLATION.value : 40, + + DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value : 30, + DeviceTypeEnum.OPEN_LINE_SYSTEM.value : 30, + + DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value : 10, + DeviceTypeEnum.PACKET_RADIO_ROUTER.value : 10, + DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER.value : 10, + DeviceTypeEnum.OPTICAL_TRANSPONDER.value : 10, + DeviceTypeEnum.EMULATED_OPTICAL_ROADM.value : 10, + DeviceTypeEnum.OPTICAL_ROADM.value : 10, + + DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER.value : 0, +} + +IGNORED_DEVICE_TYPES = {DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER} + +def get_device_manager_uuid( + device : Device +) -> Optional[str]: + for config_rule in device.device_config.config_rules: + if config_rule.WhichOneof('config_rule') != 'custom': continue + if config_rule.custom.resource_key != '_manager': continue + device_manager_id = json.loads(config_rule.custom.resource_value) + return device_manager_id['uuid'] + return None + +def _map_device_type(device : Device) -> DeviceTypeEnum: + device_type = DeviceTypeEnum._value2member_map_.get(device.device_type) # pylint: disable=no-member + if device_type is None: + MSG = 'Unsupported DeviceType({:s}) for Device({:s})' + raise Exception(MSG.format(str(device.device_type), grpc_message_to_json_string(device))) + return device_type + +def _map_resource_to_deepness(device_type : DeviceTypeEnum) -> int: + deepness = DEVICE_TYPE_TO_DEEPNESS.get(device_type.value) + if deepness is None: raise Exception('Unsupported DeviceType({:s})'.format(str(device_type.value))) + return deepness + +def get_device_type( + device : Device, device_dict : Dict[str, Tuple[Dict, Device]], device_manager_uuid : Optional[str] +) -> DeviceTypeEnum: + if device_manager_uuid is None: return _map_device_type(device) + device_manager_tuple = device_dict.get(device_manager_uuid) + if device_manager_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_manager_uuid))) + _,device = device_manager_tuple + return _map_device_type(device) + +def get_resource_classification( + device : Device, device_dict : Dict[str, Tuple[Dict, Device]] +) -> Tuple[int, DeviceTypeEnum, Optional[str]]: + device_manager_uuid = get_device_manager_uuid(device) + device_type = get_device_type(device, device_dict, device_manager_uuid) + resource_deepness = _map_resource_to_deepness(device_type) + return resource_deepness, device_type, device_manager_uuid diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py new file mode 100644 index 0000000000000000000000000000000000000000..463b8039b6c8c611b579bdb74933c06fb0f99507 --- /dev/null +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -0,0 +1,53 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import ServiceTypeEnum + +PACKET_DEVICE_TYPES = { + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER, + DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, +} + +L2_DEVICE_TYPES = { + DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM, + DeviceTypeEnum.PACKET_RADIO_ROUTER, DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER, + DeviceTypeEnum.P4_SWITCH, DeviceTypeEnum.EMULATED_P4_SWITCH, +} + +OPTICAL_DEVICE_TYPES = { + DeviceTypeEnum.OPEN_LINE_SYSTEM, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM, + DeviceTypeEnum.XR_CONSTELLATION, DeviceTypeEnum.EMULATED_XR_CONSTELLATION, + DeviceTypeEnum.OPTICAL_ROADM, DeviceTypeEnum.EMULATED_OPTICAL_ROADM, + DeviceTypeEnum.OPTICAL_TRANSPONDER, DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, +} + +SERVICE_TYPE_L2NM = {ServiceTypeEnum.SERVICETYPE_L2NM} +SERVICE_TYPE_L3NM = {ServiceTypeEnum.SERVICETYPE_L3NM} +SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM} +SERVICE_TYPE_TAPI = {ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE} + +def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum: + if device_type in PACKET_DEVICE_TYPES and prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type + if device_type in L2_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_L2NM + if device_type in OPTICAL_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE + + str_fields = ', '.join([ + 'device_type={:s}'.format(str(device_type)), + 'prv_service_type={:s}'.format(str(prv_service_type)), + ]) + raise Exception('Undefined Service Type for ({:s})'.format(str_fields)) diff --git a/test_pathcomp/ComputeSubServices.py b/test_pathcomp/ComputeSubServices.py index e0b229625838dd3f9300f4fde774b310c7511f53..1f2b4df9ca91f8588d947608896316da753dd0be 100644 --- a/test_pathcomp/ComputeSubServices.py +++ b/test_pathcomp/ComputeSubServices.py @@ -30,167 +30,41 @@ # ] # # connections=[ -# (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), <DeviceLayerEnum.OPTICAL_CONTROLLER: 1>, [ +# (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, [ # {'device': 'TN-OLS', 'ingress_ep': '833760219d0f', 'egress_ep': 'cf176771a4b9'} # ], []), -# (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), <DeviceLayerEnum.PACKET_DEVICE: 30>, [ +# (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), ServiceTypeEnum.SERVICETYPE_L2NM, [ # {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'}, # {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'}, # {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'}, # {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'} # ], [UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e')]), -# (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), <DeviceLayerEnum.APPLICATION_DEVICE: 40>, [ +# (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), ServiceTypeEnum.SERVICETYPE_L2NM, [ # {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'}, # {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'} # ], [UUID('c2e57966-5d82-4705-a5fe-44cf6487219e')]) # ] -import enum, json, queue, uuid +import logging, queue, uuid from typing import Dict, List, Optional, Tuple from common.DeviceTypes import DeviceTypeEnum -from common.proto.context_pb2 import Device, ServiceTypeEnum #, DeviceDriverEnum as grpc_DeviceDriverEnum -#from .ConstantsMappings import DEVICE_TYPE_TO_LAYER, DeviceLayerEnum - -class StackActionEnum(enum.Enum): - PATH_INGRESS = 'ingress' - CREATE_CONNECTION = 'create' - APPEND_PATH_HOP = 'append' - CHAIN_CONNECTION = 'chain' - TERMINATE_CONNECTION = 'terminate' - -#class DeviceDriverEnum(enum.IntEnum): -# EMULATED = grpc_DeviceDriverEnum.DEVICEDRIVER_UNDEFINED -# OPENCONFIG = grpc_DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG -# TRANSPORT_API = grpc_DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API -# P4 = grpc_DeviceDriverEnum.DEVICEDRIVER_P4 -# IETF_NETWORK_TOPOLOGY = grpc_DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY -# ONF_TR_352 = grpc_DeviceDriverEnum.DEVICEDRIVER_ONF_TR_352 -# XR = grpc_DeviceDriverEnum.DEVICEDRIVER_XR -# IETF_L2VPN = grpc_DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN - -def is_datacenter(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.DATACENTER, DeviceTypeEnum.EMULATED_DATACENTER} - -def is_packet_router(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER} - -def is_packet_switch(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH} - -def is_packet_device(dev_type : Optional[DeviceTypeEnum]) -> bool: - return is_packet_router(dev_type) or is_packet_switch(dev_type) - -def is_tfs_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.TERAFLOWSDN_CONTROLLER} - -def is_mw_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM} - -def is_ipm_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.XR_CONSTELLATION, DeviceTypeEnum.EMULATED_XR_CONSTELLATION} - -def is_ols_controller(dev_type : Optional[DeviceTypeEnum]) -> bool: - return dev_type in {DeviceTypeEnum.OPEN_LINE_SYSTEM, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM} - -def is_subdevice(dev_manager : Optional[str]) -> bool: - return dev_manager is not None - -def is_subdevice_equal(dev_manager_a : Optional[str], dev_manager_b : Optional[str]) -> bool: - if dev_manager_a is None and dev_manager_b is None: return True - if dev_manager_a is not None and dev_manager_b is not None: return dev_manager_a == dev_manager_b - return False - -#def has_driver(dev_drivers : List[DeviceDriverEnum], dev_driver : DeviceDriverEnum) -> bool: -# return dev_driver in dev_drivers - -def get_action( - prv_type : Optional[DeviceTypeEnum], prv_manager : Optional[str], - cur_type : DeviceTypeEnum, cur_manager : Optional[str] -) -> StackActionEnum: - if prv_type is None: - return StackActionEnum.PATH_INGRESS - - if is_datacenter(prv_type): - if is_packet_device(cur_type): return StackActionEnum.CREATE_CONNECTION - if is_tfs_controller(cur_type): return StackActionEnum.CREATE_CONNECTION - - if is_packet_device(prv_type): - if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_packet_device(cur_type): - if is_subdevice_equal(cur_manager, prv_manager): return StackActionEnum.APPEND_PATH_HOP - if is_subdevice(prv_manager) and not is_subdevice(cur_manager): return StackActionEnum.TERMINATE_CONNECTION - if not is_subdevice(prv_manager) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - - if is_mw_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - if is_ols_controller(cur_type) and not is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - if is_tfs_controller(cur_type) and is_subdevice(cur_manager): return StackActionEnum.CREATE_CONNECTION - - if is_mw_controller(prv_type) or is_ols_controller(prv_type): - if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION - - if is_tfs_controller(prv_type): - if is_tfs_controller(cur_type) and is_subdevice_equal(prv_manager, cur_manager): return StackActionEnum.APPEND_PATH_HOP - if is_datacenter(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_packet_device(cur_type): return StackActionEnum.TERMINATE_CONNECTION - if is_mw_controller(cur_type) or is_ols_controller(cur_type): return StackActionEnum.CHAIN_CONNECTION - - str_fields = ', '.join([ - 'prv_type={:s}'.format(str(prv_type)), 'prv_manager={:s}'.format(str(prv_manager)), - 'cur_type={:s}'.format(str(cur_type)), 'cur_manager={:s}'.format(str(cur_manager)), - ]) - raise Exception('Undefined Action for ({:s})'.format(str_fields)) - -def get_device_manager_uuid(device : Device) -> Optional[str]: - for config_rule in device.device_config.config_rules: - if config_rule.WhichOneof('config_rule') != 'custom': continue - if config_rule.custom.resource_key != '_manager': continue - device_manager_id = json.loads(config_rule.custom.resource_value) - return device_manager_id['uuid'] - return None - -def get_device_type( - grpc_device : Device, device_dict : Dict[str, Tuple[Dict, Device]], device_manager_uuid : Optional[str] -) -> DeviceTypeEnum: - if device_manager_uuid is None: - return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member - device_manager_tuple = device_dict.get(device_manager_uuid) - if device_manager_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_manager_uuid))) - _,grpc_device = device_manager_tuple - return DeviceTypeEnum._value2member_map_[grpc_device.device_type] # pylint: disable=no-member - - #manager_drivers = set(grpc_device.device_drivers) - #if DeviceDriverEnum.DEVICEDRIVER_IETF_L2VPN in manager_drivers: - # device_layer = DeviceLayerEnum.MAC_LAYER_CONTROLLER - #else: - # device_type = json_device['device_type'] - # device_layer = DEVICE_TYPE_TO_LAYER.get(device_type) - # if device_layer is None: raise Exception('Undefined Layer for DeviceType({:s})'.format(str(device_type))) - -SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM} - -def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum: - if is_tfs_controller(device_type) or is_packet_router(device_type): - if prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type - if is_packet_switch(device_type) or is_mw_controller(device_type): - if prv_service_type == ServiceTypeEnum.SERVICETYPE_L2NM: return prv_service_type - if is_ols_controller(device_type) or is_ipm_controller(device_type): - return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE - - str_fields = ', '.join([ - 'device_type={:s}'.format(str(device_type)), - ]) - raise Exception('Undefined Service Type for ({:s})'.format(str_fields)) +from common.proto.context_pb2 import Device, ServiceTypeEnum +from .ResourceGroups import IGNORED_DEVICE_TYPES, get_resource_classification +from .ServiceTypes import get_service_type + +LOGGER = logging.getLogger(__name__) def convert_explicit_path_hops_to_connections( path_hops : List[Dict], device_dict : Dict[str, Tuple[Dict, Device]], main_service_uuid : str, main_service_type : ServiceTypeEnum ) -> List[Tuple[str, int, List[str], List[str]]]: + LOGGER.debug('path_hops={:s}'.format(str(path_hops))) + connection_stack = queue.LifoQueue() connections : List[Tuple[str, int, List[str], List[str]]] = list() prv_device_uuid = None - prv_device_type = None - prv_manager_uuid = None + prv_res_class : Tuple[Optional[int], Optional[DeviceTypeEnum], Optional[str]] = None, None, None for path_hop in path_hops: device_uuid = path_hop['device'] @@ -199,29 +73,35 @@ def convert_explicit_path_hops_to_connections( if device_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_uuid))) _,grpc_device = device_tuple - manager_uuid = get_device_manager_uuid(grpc_device) - device_type = get_device_type(grpc_device, device_dict, manager_uuid) - action = get_action(prv_device_type, prv_manager_uuid, device_type, manager_uuid) + res_class = get_resource_classification(grpc_device, device_dict) + if res_class[1] in IGNORED_DEVICE_TYPES: continue - if action == StackActionEnum.PATH_INGRESS: + if prv_res_class[0] is None: + # path ingress connection_stack.put((main_service_uuid, main_service_type, [path_hop], [])) - elif action == StackActionEnum.CREATE_CONNECTION: - connection_uuid = str(uuid.uuid4()) - prv_service_type = connection_stack.queue[-1][1] - service_type = get_service_type(device_type, prv_service_type) - connection_stack.put((connection_uuid, service_type, [path_hop], [])) - elif action == StackActionEnum.APPEND_PATH_HOP: - connection_stack.queue[-1][2].append(path_hop) - elif action == StackActionEnum.CHAIN_CONNECTION: - connection = connection_stack.get() - connections.append(connection) - connection_stack.queue[-1][3].append(connection[0]) - + elif prv_res_class[0] > res_class[0]: + # create underlying connection connection_uuid = str(uuid.uuid4()) prv_service_type = connection_stack.queue[-1][1] - service_type = get_service_type(device_type, prv_service_type) + service_type = get_service_type(res_class[1], prv_service_type) connection_stack.put((connection_uuid, service_type, [path_hop], [])) - elif action == StackActionEnum.TERMINATE_CONNECTION: + elif prv_res_class[0] == res_class[0]: + # same resource group kind + if prv_res_class[1] == res_class[1] and prv_res_class[2] == res_class[2]: + # same device type and device manager: connection continues + connection_stack.queue[-1][2].append(path_hop) + else: + # different device type or device manager: chain connections + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + + connection_uuid = str(uuid.uuid4()) + prv_service_type = connection_stack.queue[-1][1] + service_type = get_service_type(res_class[1], prv_service_type) + connection_stack.put((connection_uuid, service_type, [path_hop], [])) + elif prv_res_class[0] < res_class[0]: + # underlying connection ended connection = connection_stack.get() connections.append(connection) connection_stack.queue[-1][3].append(connection[0]) @@ -230,11 +110,11 @@ def convert_explicit_path_hops_to_connections( raise Exception('Uncontrolled condition') prv_device_uuid = device_uuid - prv_device_type = device_type - prv_manager_uuid = manager_uuid + prv_res_class = res_class # path egress connections.append(connection_stack.get()) + LOGGER.debug('connections={:s}'.format(str(connections))) assert connection_stack.empty() return connections diff --git a/test_pathcomp/ResourceGroups.py b/test_pathcomp/ResourceGroups.py new file mode 100644 index 0000000000000000000000000000000000000000..7b38881443859417df8882c9c727114ce646239c --- /dev/null +++ b/test_pathcomp/ResourceGroups.py @@ -0,0 +1,93 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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 json +from typing import Dict, Optional, Tuple +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import Device +from common.tools.grpc.Tools import grpc_message_to_json_string + +DEVICE_TYPE_TO_DEEPNESS = { + DeviceTypeEnum.EMULATED_DATACENTER.value : 90, + DeviceTypeEnum.DATACENTER.value : 90, + DeviceTypeEnum.NETWORK.value : 90, + + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value : 70, + DeviceTypeEnum.PACKET_ROUTER.value : 70, + + DeviceTypeEnum.EMULATED_PACKET_SWITCH.value : 60, + DeviceTypeEnum.PACKET_SWITCH.value : 60, + DeviceTypeEnum.EMULATED_P4_SWITCH.value : 60, + DeviceTypeEnum.P4_SWITCH.value : 60, + + DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value : 40, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value : 40, + + DeviceTypeEnum.EMULATED_XR_CONSTELLATION.value : 40, + DeviceTypeEnum.XR_CONSTELLATION.value : 40, + + DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value : 30, + DeviceTypeEnum.OPEN_LINE_SYSTEM.value : 30, + + DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value : 10, + DeviceTypeEnum.PACKET_RADIO_ROUTER.value : 10, + DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER.value : 10, + DeviceTypeEnum.OPTICAL_TRANSPONDER.value : 10, + DeviceTypeEnum.EMULATED_OPTICAL_ROADM.value : 10, + DeviceTypeEnum.OPTICAL_ROADM.value : 10, + + DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER.value : 0, +} + +IGNORED_DEVICE_TYPES = {DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER} + +def get_device_manager_uuid( + device : Device +) -> Optional[str]: + for config_rule in device.device_config.config_rules: + if config_rule.WhichOneof('config_rule') != 'custom': continue + if config_rule.custom.resource_key != '_manager': continue + device_manager_id = json.loads(config_rule.custom.resource_value) + return device_manager_id['uuid'] + return None + +def _map_device_type(device : Device) -> DeviceTypeEnum: + device_type = DeviceTypeEnum._value2member_map_.get(device.device_type) # pylint: disable=no-member + if device_type is None: + MSG = 'Unsupported DeviceType({:s}) for Device({:s})' + raise Exception(MSG.format(str(device.device_type), grpc_message_to_json_string(device))) + return device_type + +def _map_resource_to_deepness(device_type : DeviceTypeEnum) -> int: + deepness = DEVICE_TYPE_TO_DEEPNESS.get(device_type.value) + if deepness is None: raise Exception('Unsupported DeviceType({:s})'.format(str(device_type.value))) + return deepness + +def get_device_type( + device : Device, device_dict : Dict[str, Tuple[Dict, Device]], device_manager_uuid : Optional[str] +) -> DeviceTypeEnum: + if device_manager_uuid is None: return _map_device_type(device) + device_manager_tuple = device_dict.get(device_manager_uuid) + if device_manager_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_manager_uuid))) + _,device = device_manager_tuple + return _map_device_type(device) + +def get_resource_classification( + device : Device, device_dict : Dict[str, Tuple[Dict, Device]] +) -> Tuple[int, DeviceTypeEnum, Optional[str]]: + device_manager_uuid = get_device_manager_uuid(device) + device_type = get_device_type(device, device_dict, device_manager_uuid) + resource_deepness = _map_resource_to_deepness(device_type) + return resource_deepness, device_type, device_manager_uuid diff --git a/test_pathcomp/ServiceTypes.py b/test_pathcomp/ServiceTypes.py new file mode 100644 index 0000000000000000000000000000000000000000..463b8039b6c8c611b579bdb74933c06fb0f99507 --- /dev/null +++ b/test_pathcomp/ServiceTypes.py @@ -0,0 +1,53 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import ServiceTypeEnum + +PACKET_DEVICE_TYPES = { + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER, + DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, +} + +L2_DEVICE_TYPES = { + DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM, + DeviceTypeEnum.PACKET_RADIO_ROUTER, DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER, + DeviceTypeEnum.P4_SWITCH, DeviceTypeEnum.EMULATED_P4_SWITCH, +} + +OPTICAL_DEVICE_TYPES = { + DeviceTypeEnum.OPEN_LINE_SYSTEM, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM, + DeviceTypeEnum.XR_CONSTELLATION, DeviceTypeEnum.EMULATED_XR_CONSTELLATION, + DeviceTypeEnum.OPTICAL_ROADM, DeviceTypeEnum.EMULATED_OPTICAL_ROADM, + DeviceTypeEnum.OPTICAL_TRANSPONDER, DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, +} + +SERVICE_TYPE_L2NM = {ServiceTypeEnum.SERVICETYPE_L2NM} +SERVICE_TYPE_L3NM = {ServiceTypeEnum.SERVICETYPE_L3NM} +SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM} +SERVICE_TYPE_TAPI = {ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE} + +def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum: + if device_type in PACKET_DEVICE_TYPES and prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type + if device_type in L2_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_L2NM + if device_type in OPTICAL_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE + + str_fields = ', '.join([ + 'device_type={:s}'.format(str(device_type)), + 'prv_service_type={:s}'.format(str(prv_service_type)), + ]) + raise Exception('Undefined Service Type for ({:s})'.format(str_fields)) diff --git a/test_pathcomp/data.py b/test_pathcomp/data.py index 5e94d969e31e86a572300483ca9b603cbf1d1417..399c5ff601e0ba183a12f8e3773a542ab5a5cac5 100644 --- a/test_pathcomp/data.py +++ b/test_pathcomp/data.py @@ -15,101 +15,56 @@ import json from typing import Dict, Tuple +from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import ConfigActionEnum, Device path_hops = [ - {'device': 'DC1', 'ingress_ep': 'int', 'egress_ep': 'eth1'}, - {'device': 'PE1', 'ingress_ep': '1/1', 'egress_ep': '1/2'}, - {'device': 'MW1-2', 'ingress_ep': '172.18.0.1:1', 'egress_ep': '172.18.0.2:1'}, - {'device': 'R1', 'ingress_ep': '1/1', 'egress_ep': '1/3'}, - {'device': 'OLS', 'ingress_ep': 'aade6001-f00b-5e2f-a357-6a0a9d3de870', 'egress_ep': '0ef74f99-1acc-57bd-ab9d-4b958b06c513'}, - {'device': 'R2', 'ingress_ep': '1/1', 'egress_ep': '1/2'}, - {'device': 'PE3', 'ingress_ep': '1/1', 'egress_ep': '1/2'}, - {'device': 'DC2', 'ingress_ep': 'eth1', 'egress_ep': 'int'} + {'device': 'DC1', 'ingress_ep': 'int', 'egress_ep': 'eth1' }, + {'device': 'PE1', 'ingress_ep': '1/1', 'egress_ep': '1/2' }, + {'device': 'MW1-2', 'ingress_ep': '172.18.0.1:1', 'egress_ep': '172.18.0.2:1' }, + {'device': 'HUB1', 'ingress_ep': '1/1', 'egress_ep': 'XR-T1' }, + {'device': 'splitter', 'ingress_ep': 'common', 'egress_ep': 'leaf1' }, + {'device': 'OLS', 'ingress_ep': 'node_1_port_13-input', 'egress_ep': 'node_4_port_13-output'}, + {'device': 'LEAF2', 'ingress_ep': 'XR-T1', 'egress_ep': '1/1' }, + {'device': 'PE4', 'ingress_ep': '1/1', 'egress_ep': '1/2' }, + {'device': 'DC2', 'ingress_ep': 'eth2', 'egress_ep': 'int' } ] -device_dict = { - 'R3': {'device_Id': 'R3', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'R3', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'R3', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'} - ]}, - 'PE4': {'device_Id': 'PE4', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'PE4', 'endpoint_uuid': 'mgmt'}, 'endpoint_type': 'mgmt'}, - {'endpoint_id': {'device_id': 'PE4', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'PE4', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'} - ]}, - 'PE2': {'device_Id': 'PE2', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'PE2', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'PE2', 'endpoint_uuid': 'mgmt'}, 'endpoint_type': 'mgmt'}, - {'endpoint_id': {'device_id': 'PE2', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'} - ]}, - 'R1': {'device_Id': 'R1', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'R1', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'R1', 'endpoint_uuid': '1/3'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'R1', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'} - ]}, - 'PE3': {'device_Id': 'PE3', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'PE3', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'PE3', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'PE3', 'endpoint_uuid': 'mgmt'}, 'endpoint_type': 'mgmt'} - ]}, - 'OLS': {'device_Id': 'OLS', 'device_type': 'emu-open-line-system', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'OLS', 'endpoint_uuid': '0ef74f99-1acc-57bd-ab9d-4b958b06c513'}, 'endpoint_type': 'optical'}, - {'endpoint_id': {'device_id': 'OLS', 'endpoint_uuid': '50296d99-58cc-5ce7-82f5-fc8ee4eec2ec'}, 'endpoint_type': 'optical'}, - {'endpoint_id': {'device_id': 'OLS', 'endpoint_uuid': 'aade6001-f00b-5e2f-a357-6a0a9d3de870'}, 'endpoint_type': 'optical'}, - {'endpoint_id': {'device_id': 'OLS', 'endpoint_uuid': 'eb287d83-f05e-53ec-ab5a-adf6bd2b5418'}, 'endpoint_type': 'optical'} - ]}, - 'PE1': {'device_Id': 'PE1', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'PE1', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'PE1', 'endpoint_uuid': 'mgmt'}, 'endpoint_type': 'mgmt'}, - {'endpoint_id': {'device_id': 'PE1', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'} - ]}, - 'DC2': {'device_Id': 'DC2', 'device_type': 'emu-datacenter', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'DC2', 'endpoint_uuid': 'eth1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'DC2', 'endpoint_uuid': 'eth2'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'DC2', 'endpoint_uuid': 'int'}, 'endpoint_type': 'copper/internal'} - ]}, - 'MW1-2': {'device_Id': 'MW1-2', 'device_type': 'emu-microwave-radio-system', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'MW1-2', 'endpoint_uuid': '172.18.0.1:1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'MW1-2', 'endpoint_uuid': '172.18.0.2:1'}, 'endpoint_type': 'copper/internal'} - ]}, - 'MW3-4': {'device_Id': 'MW3-4', 'device_type': 'emu-microwave-radio-system', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'MW3-4', 'endpoint_uuid': '172.18.0.3:1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'MW3-4', 'endpoint_uuid': '172.18.0.4:1'}, 'endpoint_type': 'copper/internal'} - ]}, - 'TFS': {'device_Id': 'TFS', 'device_type': 'teraflowsdn', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'TFS', 'endpoint_uuid': 'mgmt'}, 'endpoint_type': 'mgmt'} - ]}, - 'R2': {'device_Id': 'R2', 'device_type': 'emu-packet-router', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'R2', 'endpoint_uuid': '1/2'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'R2', 'endpoint_uuid': '1/1'}, 'endpoint_type': 'copper/internal'} - ]}, - 'DC1': {'device_Id': 'DC1', 'device_type': 'emu-datacenter', 'device_endpoints': [ - {'endpoint_id': {'device_id': 'DC1', 'endpoint_uuid': 'int'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'DC1', 'endpoint_uuid': 'eth1'}, 'endpoint_type': 'copper/internal'}, - {'endpoint_id': {'device_id': 'DC1', 'endpoint_uuid': 'eth2'}, 'endpoint_type': 'copper/internal'} - ]}, -} +device_data = { + 'TFS' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.TERAFLOWSDN_CONTROLLER }, + 'IPM' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.XR_CONSTELLATION }, + 'OLS' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.OPEN_LINE_SYSTEM }, + 'MW1-2' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM }, + 'MW3-4' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM }, -MANAGED_DEVICES = {'PE1', 'PE2', 'PE3', 'PE4'} -MANAGER = 'TFS' + 'DC1' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.EMULATED_DATACENTER }, + 'DC2' : {'manager_uuid': None, 'device_type': DeviceTypeEnum.EMULATED_DATACENTER }, -def process_device(json_device) -> Tuple[Dict, Device]: - device_uuid = json_device['device_Id'] + 'PE1' : {'manager_uuid': 'TFS', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, + 'PE2' : {'manager_uuid': 'TFS', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, + 'PE3' : {'manager_uuid': 'TFS', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, + 'PE4' : {'manager_uuid': 'TFS', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, - grpc_device = Device() - grpc_device.device_id.device_uuid.uuid = device_uuid - grpc_device.device_type = json_device['device_type'] + 'HUB1' : {'manager_uuid': 'IPM', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, + 'LEAF1' : {'manager_uuid': 'IPM', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, + 'LEAF2' : {'manager_uuid': 'IPM', 'device_type': DeviceTypeEnum.PACKET_ROUTER }, - if device_uuid in MANAGED_DEVICES: - config_rule = grpc_device.device_config.config_rules.add() + 'splitter': {'manager_uuid': None, 'device_type': DeviceTypeEnum.EMULATED_OPTICAL_SPLITTER}, +} + +def process_device(device_uuid, json_device) -> Tuple[Dict, Device]: + grpc_device = Device() + grpc_device.device_id.device_uuid.uuid = device_uuid # pylint: disable=no-member + grpc_device.device_type = json_device['device_type'].value + manager_uuid = json_device.get('manager_uuid') + if manager_uuid is not None: + config_rule = grpc_device.device_config.config_rules.add() # pylint: disable=no-member config_rule.action = ConfigActionEnum.CONFIGACTION_SET config_rule.custom.resource_key = '_manager' - config_rule.custom.resource_value = json.dumps({'uuid': MANAGER}) - + config_rule.custom.resource_value = json.dumps({'uuid': manager_uuid}) return json_device, grpc_device device_dict = { - device_uuid:process_device(json_device) - for device_uuid,json_device in device_dict.items() + device_uuid:process_device(device_uuid, json_device) + for device_uuid,json_device in device_data.items() } diff --git a/test_pathcomp/old_ComputeSubServices.py b/test_pathcomp/old_ComputeSubServices.py new file mode 100644 index 0000000000000000000000000000000000000000..c1d3115d4c72e807e0d68c9caeacd153331e8c4f --- /dev/null +++ b/test_pathcomp/old_ComputeSubServices.py @@ -0,0 +1,119 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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. + +# Convert the path defined as explicit hops with ingress and egress endpoints per device into a set of connections and +# compute the dependencies among them. +# +# Example: +# o-- int DC1 eth1 -- 10/1 CS1 1/2 -- 1/2 R2 2/1 -- a7.. OLS 60.. -- 2/1 R3 1/1 -- 1/1 CS2 10/1 -- eth1 DC2 int --o +# APP PKT PKT CTRL PKT PKT APP +# +# path_hops = [ +# {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'}, +# {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'}, +# {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'}, +# {'device': 'TN-OLS', 'ingress_ep': 'a7a80b23a703', 'egress_ep': '60519106029e'}, +# {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'}, +# {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'}, +# {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'} +# ] +# +# connections=[ +# (UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e'), ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, [ +# {'device': 'TN-OLS', 'ingress_ep': '833760219d0f', 'egress_ep': 'cf176771a4b9'} +# ], []), +# (UUID('c2e57966-5d82-4705-a5fe-44cf6487219e'), ServiceTypeEnum.SERVICETYPE_L2NM, [ +# {'device': 'CS1-GW1', 'ingress_ep': '10/1', 'egress_ep': '1/2'}, +# {'device': 'TN-R2', 'ingress_ep': '1/2', 'egress_ep': '2/1'}, +# {'device': 'TN-R3', 'ingress_ep': '2/1', 'egress_ep': '1/1'}, +# {'device': 'CS2-GW1', 'ingress_ep': '1/1', 'egress_ep': '10/1'} +# ], [UUID('7548edf7-ee7c-4adf-ac0f-c7a0c0dfba8e')]), +# (UUID('1e205c82-f6ea-4977-9e97-dc27ef1f4802'), ServiceTypeEnum.SERVICETYPE_L2NM, [ +# {'device': 'DC1-GW', 'ingress_ep': 'int', 'egress_ep': 'eth1'}, +# {'device': 'DC2-GW', 'ingress_ep': 'eth1', 'egress_ep': 'int'} +# ], [UUID('c2e57966-5d82-4705-a5fe-44cf6487219e')]) +# ] + +import enum, json, logging, queue, uuid +from typing import Dict, List, Optional, Tuple +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import Device, ServiceTypeEnum +from test_pathcomp.ResourceGroups import ResourceGroupKindEnum + + + +from .ConstantsMappings import DEVICE_TYPE_TO_LAYER, DeviceLayerEnum + +def convert_explicit_path_hops_to_connections( + path_hops : List[Dict], device_dict : Dict[str, Tuple[Dict, Device]], + main_service_uuid : str, main_service_type : ServiceTypeEnum +) -> List[Tuple[str, int, List[str], List[str]]]: + + connection_stack = queue.LifoQueue() + connections : List[Tuple[str, int, List[str], List[str]]] = list() + prv_device_uuid = None + prv_resource_group : Optional[Tuple[ResourceGroupKindEnum, DeviceTypeEnum, str]] = None + + for path_hop in path_hops: + device_uuid = path_hop['device'] + if prv_device_uuid == device_uuid: continue + device_tuple = device_dict.get(device_uuid) + if device_tuple is None: raise Exception('Device({:s}) not found'.format(str(device_uuid))) + json_device,_ = device_tuple + device_type = json_device['device_type'] + resource_group = DEVICE_TYPE_TO_LAYER.get(device_type) + if resource_group is None: raise Exception('Undefined Layer for DeviceType({:s})'.format(str(device_type))) + + if prv_resource_group is None: + # path ingress + connection_stack.put((main_service_uuid, main_service_type, [path_hop], [])) + elif prv_resource_group > resource_group: + # underlying connection begins + connection_uuid = str(uuid.uuid4()) + connection_stack.put((connection_uuid, resource_group, [path_hop], [])) + elif prv_resource_group == resource_group: + # same connection continues + connection_stack.queue[-1][2].append(path_hop) + elif prv_resource_group < resource_group: + # underlying connection ended + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + connection_stack.queue[-1][2].append(path_hop) + else: + raise Exception('Uncontrolled condition') + + prv_resource_group = resource_group + prv_device_uuid = device_uuid + + # path egress + connections.append(connection_stack.get()) + assert connection_stack.empty() + return connections + +def convert_explicit_path_hops_to_plain_connection( + path_hops : List[Dict], main_service_uuid : str, main_service_type : ServiceTypeEnum +) -> List[Tuple[str, int, List[str], List[str]]]: + + connection : Tuple[str, int, List[str], List[str]] = \ + (main_service_uuid, main_service_type, [], []) + + prv_device_uuid = None + for path_hop in path_hops: + device_uuid = path_hop['device'] + if prv_device_uuid == device_uuid: continue + connection[2].append(path_hop) + prv_device_uuid = device_uuid + + return [connection]