diff --git a/src/common/tools/object_factory/EndPoint.py b/src/common/tools/object_factory/EndPoint.py index 326c67e1e942e5c4056389a633c70e2830737217..a38ad0d5c59ee75742459729003d43ef01612f53 100644 --- a/src/common/tools/object_factory/EndPoint.py +++ b/src/common/tools/object_factory/EndPoint.py @@ -30,7 +30,7 @@ def json_endpoint_ids( def json_endpoint( device_id : Dict, endpoint_uuid : str, endpoint_type : str, topology_id : Optional[Dict] = None, - kpi_sample_types : List[int] = [] + kpi_sample_types : List[int] = [], location : Optional[Dict] = None ): result = { @@ -38,6 +38,7 @@ def json_endpoint( 'endpoint_type': endpoint_type, } if len(kpi_sample_types) > 0: result['kpi_sample_types'] = copy.deepcopy(kpi_sample_types) + if location: result['endpoint_location'] = copy.deepcopy(location) return result def json_endpoints( diff --git a/src/common/tools/object_factory/Location.py b/src/common/tools/object_factory/Location.py new file mode 100644 index 0000000000000000000000000000000000000000..ac44ef4c64d8466be123c5a722bf7fe69f5e48a8 --- /dev/null +++ b/src/common/tools/object_factory/Location.py @@ -0,0 +1,30 @@ +# 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 copy +from typing import Dict, Optional + + +def json_gps_position(latitude : float, longitude : float): + return {'latitude': latitude, 'longitude': longitude} + +def json_location(region : Optional[str] = None, gps_position : Optional[Dict] = None): + if not region and not gps_position: + raise Exception('One of "region" or "gps_position" arguments must be filled') + if region: + result = {'region': region} + else: + result = {'gps_position': copy.deepcopy(gps_position)} + + return result diff --git a/src/service/requirements.in b/src/service/requirements.in index 83b6342c0c0ed5e969ba03e8af5ac502b1f525c9..bb321e22a1bd56df4193d06c0481acc44640ea7e 100644 --- a/src/service/requirements.in +++ b/src/service/requirements.in @@ -17,3 +17,4 @@ anytree==2.8.0 networkx==2.6.3 pydot==1.4.2 redis==4.1.2 +geopy==2.3.0 diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index 0b2e0760161c109a2ba6a5feecc931e8bcf5c14f..767ba2a58ac73673df5e0c24c2c3e952da471fac 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -25,6 +25,7 @@ from pathcomp.frontend.client.PathCompClient import PathCompClient from .service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory from .task_scheduler.TaskScheduler import TasksScheduler from .tools.ContextGetters import get_service +from .tools.GeodesicDistance import gps_distance LOGGER = logging.getLogger(__name__) @@ -113,21 +114,42 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): if constraint.WhichOneof('constraint') == 'sla_availability': num_disjoint_paths = constraint.sla_availability.num_disjoint_paths break + service_locations = [] + for constraint in request.service_constraints: + if constraint.WhichOneof('constraint') == 'endpoint_location': + service_locations.append(constraint.endpoint_location.location) + num_disjoint_paths = 1 if num_disjoint_paths is None or num_disjoint_paths == 0 else num_disjoint_paths num_expected_endpoints = num_disjoint_paths * 2 tasks_scheduler = TasksScheduler(self.service_handler_factory) - if len(service_with_uuids.service_endpoint_ids) >= num_expected_endpoints: + if len(service_with_uuids.service_endpoint_ids) >= num_expected_endpoints or\ + len(service_locations) >= num_expected_endpoints: pathcomp_request = PathCompRequest() pathcomp_request.services.append(service_with_uuids) # pylint: disable=no-member + if service_locations: + context_client = ContextClient() + device_list = context_client.ListDevices(Empty()) + + closer_endpoint_ids = [] + for service_location in service_locations: + distances = {} + for device in device_list.devices: + for endpoint in device.device_endpoints: + distances[gps_distance(service_location.gps_position, endpoint.endpoint_location.gps_position)] = endpoint.endpoint_id + min_distance = min(distances) + closer_endpoint_ids.append(distances[min_distance]) + + pathcomp_request.services[0].service_endpoint_ids.extend(closer_endpoint_ids) + if num_disjoint_paths is None or num_disjoint_paths in {0, 1}: pathcomp_request.shortest_path.Clear() # pylint: disable=no-member else: pathcomp_request.k_disjoint_path.num_disjoint = num_disjoint_paths # pylint: disable=no-member - LOGGER.debug('pathcomp_request={:s}'.format(grpc_message_to_json_string(pathcomp_request))) + LOGGER.info('pathcomp_request={:s}'.format(grpc_message_to_json_string(pathcomp_request))) pathcomp = PathCompClient() pathcomp_reply = pathcomp.Compute(pathcomp_request) pathcomp.close() diff --git a/src/service/service/tools/GeodesicDistance.py b/src/service/service/tools/GeodesicDistance.py new file mode 100644 index 0000000000000000000000000000000000000000..b66d336f0f617aa834905785e2e49f95073a2df9 --- /dev/null +++ b/src/service/service/tools/GeodesicDistance.py @@ -0,0 +1,18 @@ +# 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 geopy.distance import geodesic + +def gps_distance(gps1, gps2): + return geodesic((gps1.latitude, gps1.longitude), (gps2.latitude, gps2.longitude)).km diff --git a/src/service/tests/ServiceHandler_L3NM_EMU.py b/src/service/tests/ServiceHandler_L3NM_EMU.py index 3df27b439626dad652e443cca4195ce36f4ac86f..4e1fe5c01c57836417fc1a9e8f6436a6b32eedd5 100644 --- a/src/service/tests/ServiceHandler_L3NM_EMU.py +++ b/src/service/tests/ServiceHandler_L3NM_EMU.py @@ -13,8 +13,10 @@ # limitations under the License. from typing import Dict, List, Tuple + +from common.tools.object_factory.Location import json_location, json_gps_position from common.tools.object_factory.ConfigRule import json_config_rule_set -from common.tools.object_factory.Constraint import json_constraint_custom +from common.tools.object_factory.Constraint import json_constraint_custom, json_constraint_endpoint_location_gps from common.tools.object_factory.Device import ( json_device_emulated_packet_router_disabled, json_device_emulated_tapi_disabled, json_device_id) from common.tools.object_factory.EndPoint import json_endpoint, json_endpoint_id @@ -24,21 +26,32 @@ from .CommonObjects import CONTEXT, CONTEXT_ID, PACKET_PORT_SAMPLE_TYPES, TOPOLO SERVICE_HANDLER_NAME = 'l3nm_emulated' -def json_endpoint_ids(device_id : Dict, endpoint_descriptors : List[Tuple[str, str]]): +def json_endpoint_ids(device_id : Dict, endpoint_descriptors : List[Tuple[str, str, str]]): return [ json_endpoint_id(device_id, ep_uuid) - for ep_uuid, _ in endpoint_descriptors + for ep_uuid, _, _ in endpoint_descriptors ] -def json_endpoints(device_id : Dict, endpoint_descriptors : List[Tuple[str, str]]): +def json_endpoints(device_id : Dict, endpoint_descriptors : List[Tuple[str, str, str]]): return [ - json_endpoint(device_id, ep_uuid, ep_type, kpi_sample_types=PACKET_PORT_SAMPLE_TYPES) - for ep_uuid, ep_type in endpoint_descriptors + json_endpoint(device_id, ep_uuid, ep_type, kpi_sample_types=PACKET_PORT_SAMPLE_TYPES, location=ep_location) + for ep_uuid, ep_type, ep_location in endpoint_descriptors ] + +BARCELONA_GPS = (41.386726, 2.170107) +MALAGA_GPS = (36.721162, -4.418339) +ZARAGOZA_GPS = (41.655552, -0.876442) +MADRID_GPS = (40.416741, -3.703285) +TOLEDO_GPS = (39.862947, -4.027485) +ANDORRA_GPS = (42.506017, 1.525923) +SANTIAGO_GPS = (42.876254, -8.543588) +ALBACETE_GPS = (38.998249, -1.858145) + + # ----- Devices -------------------------------------------------------------------------------------------------------- DEVICE_R1_UUID = 'R1' -DEVICE_R1_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R1_ENDPOINT_DEFS = [('EP1', 'optical', json_location(gps_position=json_gps_position(*BARCELONA_GPS))), ('EP100', 'copper', json_location(gps_position=json_gps_position(*BARCELONA_GPS)))] DEVICE_R1_ID = json_device_id(DEVICE_R1_UUID) DEVICE_R1_ENDPOINTS = json_endpoints(DEVICE_R1_ID, DEVICE_R1_ENDPOINT_DEFS) DEVICE_R1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R1_ID, DEVICE_R1_ENDPOINT_DEFS) @@ -47,7 +60,7 @@ ENDPOINT_ID_R1_EP1 = DEVICE_R1_ENDPOINT_IDS[0] ENDPOINT_ID_R1_EP100 = DEVICE_R1_ENDPOINT_IDS[1] DEVICE_R2_UUID = 'R2' -DEVICE_R2_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R2_ENDPOINT_DEFS = [('EP1', 'optical', json_location(gps_position=json_gps_position(*MADRID_GPS))), ('EP100', 'copper', json_location(gps_position=json_gps_position(*MADRID_GPS)))] DEVICE_R2_ID = json_device_id(DEVICE_R2_UUID) DEVICE_R2_ENDPOINTS = json_endpoints(DEVICE_R2_ID, DEVICE_R2_ENDPOINT_DEFS) DEVICE_R2_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R2_ID, DEVICE_R2_ENDPOINT_DEFS) @@ -56,7 +69,7 @@ ENDPOINT_ID_R2_EP1 = DEVICE_R2_ENDPOINT_IDS[0] ENDPOINT_ID_R2_EP100 = DEVICE_R2_ENDPOINT_IDS[1] DEVICE_R3_UUID = 'R3' -DEVICE_R3_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP100', 'copper')] +DEVICE_R3_ENDPOINT_DEFS = [('EP1', 'optical', json_location(gps_position=json_gps_position(*MALAGA_GPS))), ('EP100', 'copper', json_location(gps_position=json_gps_position(*MALAGA_GPS)))] DEVICE_R3_ID = json_device_id(DEVICE_R3_UUID) DEVICE_R3_ENDPOINTS = json_endpoints(DEVICE_R3_ID, DEVICE_R3_ENDPOINT_DEFS) DEVICE_R3_ENDPOINT_IDS = json_endpoint_ids(DEVICE_R3_ID, DEVICE_R3_ENDPOINT_DEFS) @@ -65,7 +78,7 @@ ENDPOINT_ID_R3_EP1 = DEVICE_R3_ENDPOINT_IDS[0] ENDPOINT_ID_R3_EP100 = DEVICE_R3_ENDPOINT_IDS[1] DEVICE_O1_UUID = 'O1' -DEVICE_O1_ENDPOINT_DEFS = [('EP1', 'optical'), ('EP2', 'optical'), ('EP3', 'optical')] +DEVICE_O1_ENDPOINT_DEFS = [('EP1', 'optical', json_location(gps_position=json_gps_position(*ANDORRA_GPS))), ('EP2', 'optical', json_location(gps_position=json_gps_position(*ALBACETE_GPS))), ('EP3', 'optical', json_location(gps_position=json_gps_position(*SANTIAGO_GPS)))] DEVICE_O1_ID = json_device_id(DEVICE_O1_UUID) DEVICE_O1_ENDPOINTS = json_endpoints(DEVICE_O1_ID, DEVICE_O1_ENDPOINT_DEFS) DEVICE_O1_ENDPOINT_IDS = json_endpoint_ids(DEVICE_O1_ID, DEVICE_O1_ENDPOINT_DEFS) @@ -104,6 +117,12 @@ SERVICE_R1_R3_CONSTRAINTS = [ json_constraint_custom('latency_ms', 15.2), json_constraint_custom('jitter_us', 1.2), ] + + +SERVICE_R1_R3_CONSTRAINTS_LOCATION = [ + json_constraint_endpoint_location_gps(None, ZARAGOZA_GPS[0], ZARAGOZA_GPS[1]), + json_constraint_endpoint_location_gps(None, TOLEDO_GPS[0], TOLEDO_GPS[1]), +] SERVICE_R1_R3_CONFIG_RULES = [ json_config_rule_set( '/settings', @@ -123,14 +142,14 @@ SERVICE_R1_R3_DESCRIPTOR = json_service_l3nm_planned(SERVICE_R1_R3_UUID) # ----- Test Descriptor ------------------------------------------------------------------------------------------------ TEST_SERVICE_HANDLER = (SERVICE_HANDLER_NAME, { - 'contexts' : [CONTEXT], - 'topologies' : [TOPOLOGY], - 'devices' : [DEVICE_R1, DEVICE_R2, DEVICE_R3, DEVICE_O1], - 'links' : [LINK_R1_O1, LINK_R2_O1, LINK_R3_O1], - - 'service_id' : SERVICE_R1_R3_ID, - 'service_descriptor' : SERVICE_R1_R3_DESCRIPTOR, - 'service_endpoint_ids' : SERVICE_R1_R3_ENDPOINT_IDS, - 'service_config_rules' : SERVICE_R1_R3_CONFIG_RULES, - 'service_constraints' : SERVICE_R1_R3_CONSTRAINTS, + 'contexts' : [CONTEXT], + 'topologies' : [TOPOLOGY], + 'devices' : [DEVICE_R1, DEVICE_R2, DEVICE_R3, DEVICE_O1], + 'links' : [LINK_R1_O1, LINK_R2_O1, LINK_R3_O1], + 'service_id' : SERVICE_R1_R3_ID, + 'service_descriptor' : SERVICE_R1_R3_DESCRIPTOR, + 'service_endpoint_ids' : SERVICE_R1_R3_ENDPOINT_IDS, + 'service_config_rules' : SERVICE_R1_R3_CONFIG_RULES, + 'service_constraints' : SERVICE_R1_R3_CONSTRAINTS, + 'service_constraints_location' : SERVICE_R1_R3_CONSTRAINTS_LOCATION, })