From 1e992e5ce84e54ce51cadf2d072eb0cab16c3fce Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 17:38:59 +0000 Subject: [PATCH 01/19] Service component - L3NM IETF ACTN Service Handler: - Added skeleton of service handler --- .../service/service_handlers/__init__.py | 7 + .../l3nm_ietf_actn/ConfigRuleComposer.py | 128 ++++++++++++++ .../L3NMIetfActnServiceHandler.py | 161 ++++++++++++++++++ .../l3nm_ietf_actn/__init__.py | 14 ++ 4 files changed, 310 insertions(+) create mode 100644 src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py create mode 100644 src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py create mode 100644 src/service/service/service_handlers/l3nm_ietf_actn/__init__.py diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 551d35c7b..eaf8f715a 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -20,6 +20,7 @@ from .l2nm_openconfig.L2NMOpenConfigServiceHandler import L2NMOpenConfigServiceH from .l3nm_emulated.L3NMEmulatedServiceHandler import L3NMEmulatedServiceHandler from .l3nm_openconfig.L3NMOpenConfigServiceHandler import L3NMOpenConfigServiceHandler from .l3nm_gnmi_openconfig.L3NMGnmiOpenConfigServiceHandler import L3NMGnmiOpenConfigServiceHandler +from .l3nm_ietf_actn.L3NMIetfActnServiceHandler import L3NMIetfActnServiceHandler from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler from .p4.p4_service_handler import P4ServiceHandler from .tapi_tapi.TapiServiceHandler import TapiServiceHandler @@ -57,6 +58,12 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, } ]), + (L3NMIetfActnServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_L3NM, + FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, + } + ]), (TapiServiceHandler, [ { FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py new file mode 100644 index 000000000..deb096b06 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py @@ -0,0 +1,128 @@ +# 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 typing import Dict, List, Optional, Tuple +from common.proto.context_pb2 import Device, EndPoint +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set + +from service.service.service_handler_api.AnyTreeTools import TreeNode + +def _interface( + if_name : str, ipv4_address : str, ipv4_prefix_length : int, enabled : bool, + vlan_id : Optional[int] = None, sif_index : Optional[int] = 1 +) -> Tuple[str, Dict]: + str_path = '/interface[{:s}]'.format(if_name) + data = { + 'name': if_name, 'enabled': enabled, 'sub_if_index': sif_index, + 'sub_if_enabled': enabled, 'sub_if_ipv4_enabled': enabled, + 'sub_if_ipv4_address': ipv4_address, 'sub_if_ipv4_prefix_length': ipv4_prefix_length + } + if vlan_id is not None: data['sub_if_vlan'] = vlan_id + return str_path, data + +def _network_instance(ni_name, ni_type) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]'.format(ni_name) + data = {'name': ni_name, 'type': ni_type} + return str_path, data + +def _network_instance_static_route(ni_name, prefix, next_hop, next_hop_index=0) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]/static_route[{:s}]'.format(ni_name, prefix) + data = {'name': ni_name, 'prefix': prefix, 'next_hop': next_hop, 'next_hop_index': next_hop_index} + return str_path, data + +def _network_instance_interface(ni_name, if_name, sif_index) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]/interface[{:s}.{:d}]'.format(ni_name, if_name, sif_index) + data = {'name': ni_name, 'if_name': if_name, 'sif_index': sif_index} + return str_path, data + +class EndpointComposer: + def __init__(self, endpoint_uuid : str) -> None: + self.uuid = endpoint_uuid + self.objekt : Optional[EndPoint] = None + self.sub_interface_index = 0 + self.ipv4_address = None + self.ipv4_prefix_length = None + self.sub_interface_vlan_id = 0 + + def configure(self, endpoint_obj : EndPoint, settings : Optional[TreeNode]) -> None: + self.objekt = endpoint_obj + if settings is None: return + json_settings : Dict = settings.value + self.ipv4_address = json_settings['ipv4_address'] + self.ipv4_prefix_length = json_settings['ipv4_prefix_length'] + self.sub_interface_index = json_settings['sub_interface_index'] + self.sub_interface_vlan_id = json_settings['sub_interface_vlan_id'] + + def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + json_config_rule = json_config_rule_delete if delete else json_config_rule_set + return [ + json_config_rule(*_interface( + self.objekt.name, self.ipv4_address, self.ipv4_prefix_length, True, + sif_index=self.sub_interface_index, vlan_id=self.sub_interface_vlan_id, + )), + json_config_rule(*_network_instance_interface( + network_instance_name, self.objekt.name, self.sub_interface_index + )), + ] + +class DeviceComposer: + def __init__(self, device_uuid : str) -> None: + self.uuid = device_uuid + self.objekt : Optional[Device] = None + self.endpoints : Dict[str, EndpointComposer] = dict() + self.static_routes : Dict[str, str] = dict() + + def get_endpoint(self, endpoint_uuid : str) -> EndpointComposer: + if endpoint_uuid not in self.endpoints: + self.endpoints[endpoint_uuid] = EndpointComposer(endpoint_uuid) + return self.endpoints[endpoint_uuid] + + def configure(self, device_obj : Device, settings : Optional[TreeNode]) -> None: + self.objekt = device_obj + if settings is None: return + json_settings : Dict = settings.value + static_routes = json_settings.get('static_routes', []) + for static_route in static_routes: + prefix = static_route['prefix'] + next_hop = static_route['next_hop'] + self.static_routes[prefix] = next_hop + + def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + json_config_rule = json_config_rule_delete if delete else json_config_rule_set + config_rules = [ + json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) + ] + for endpoint in self.endpoints.values(): + config_rules.extend(endpoint.get_config_rules(network_instance_name, delete=delete)) + for prefix, next_hop in self.static_routes.items(): + config_rules.append( + json_config_rule(*_network_instance_static_route(network_instance_name, prefix, next_hop)) + ) + if delete: config_rules = list(reversed(config_rules)) + return config_rules + +class ConfigRuleComposer: + def __init__(self) -> None: + self.devices : Dict[str, DeviceComposer] = dict() + + def get_device(self, device_uuid : str) -> DeviceComposer: + if device_uuid not in self.devices: + self.devices[device_uuid] = DeviceComposer(device_uuid) + return self.devices[device_uuid] + + def get_config_rules(self, network_instance_name : str, delete : bool = False) -> Dict[str, List[Dict]]: + return { + device_uuid : device.get_config_rules(network_instance_name, delete=delete) + for device_uuid, device in self.devices.items() + } diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py new file mode 100644 index 000000000..4b53ac0d2 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py @@ -0,0 +1,161 @@ +# 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, logging +from typing import Any, Dict, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.task_scheduler.TaskExecutor import TaskExecutor +from .ConfigRuleComposer import ConfigRuleComposer + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_ietf_actn'}) + +class L3NMIetfActnServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + self.__composer = ConfigRuleComposer() + self.__endpoint_map : Dict[Tuple[str, str], str] = dict() + + def _compose_config_rules(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> None: + for endpoint in endpoints: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + _device = self.__composer.get_device(device_obj.name) + _device.configure(device_obj, device_settings) + + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + _endpoint = _device.get_endpoint(endpoint_obj.name) + _endpoint.configure(endpoint_obj, endpoint_settings) + + self.__endpoint_map[(device_uuid, endpoint_uuid)] = device_obj.name + + def _do_configurations( + self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], + delete : bool = False + ) -> List[Union[bool, Exception]]: + # Configuration is done atomically on each device, all OK / all KO per device + results_per_device = dict() + for device_name,json_config_rules in config_rules_per_device.items(): + try: + device_obj = self.__composer.get_device(device_name).objekt + if len(json_config_rules) == 0: continue + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + results_per_device[device_name] = True + except Exception as e: # pylint: disable=broad-exception-caught + verb = 'deconfigure' if delete else 'configure' + MSG = 'Unable to {:s} Device({:s}) : ConfigRules({:s})' + LOGGER.exception(MSG.format(verb, str(device_name), str(json_config_rules))) + results_per_device[device_name] = e + + results = [] + for endpoint in endpoints: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + device_name = self.__endpoint_map[(device_uuid, endpoint_uuid)] + results.append(results_per_device[device_name]) + return results + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + service_uuid = self.__service.service_id.service_uuid.uuid + #settings = self.__settings_handler.get('/settings') + self._compose_config_rules(endpoints) + network_instance_name = service_uuid.split('-')[0] + config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=False) + results = self._do_configurations(config_rules_per_device, endpoints) + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + service_uuid = self.__service.service_id.service_uuid.uuid + #settings = self.__settings_handler.get('/settings') + self._compose_config_rules(endpoints) + network_instance_name = service_uuid.split('-')[0] + config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=True) + results = self._do_configurations(config_rules_per_device, endpoints, delete=True) + return results + + @metered_subclass_method(METRICS_POOL) + def SetConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + resource_value = json.loads(resource[1]) + self.__settings_handler.set(resource[0], resource_value) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetConfig({:s})'.format(str(resource))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + self.__settings_handler.delete(resource[0]) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource))) + results.append(e) + + return results diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/__init__.py b/src/service/service/service_handlers/l3nm_ietf_actn/__init__.py new file mode 100644 index 000000000..1549d9811 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/__init__.py @@ -0,0 +1,14 @@ +# 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. + -- GitLab From 3ab5419e30ee5b4519cafb14f3497914699daa22 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 18:21:30 +0000 Subject: [PATCH 02/19] Tests - Tools - Mock IETF ACTN SDN Controller: - Fixed base URL --- src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py index c459c294c..26243e2b6 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py @@ -29,7 +29,7 @@ from ResourceOsuTunnels import OsuTunnel, OsuTunnels BIND_ADDRESS = '0.0.0.0' BIND_PORT = 8443 -BASE_URL = '/restconf/data' +BASE_URL = '/restconf/v2/data' STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL)) LOG_LEVEL = logging.DEBUG -- GitLab From a21008c998cb1ba460819bd0b3c4f7acf3e8b565 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 18:21:55 +0000 Subject: [PATCH 03/19] Device - IETF ACTN Driver: - Add mgmt endpoint by default --- src/device/service/drivers/ietf_actn/IetfActnDriver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index a33c403f3..5f80f5333 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -16,7 +16,7 @@ import json, logging, requests, threading from typing import Any, Iterator, List, Optional, Tuple, Union from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method from common.type_checkers.Checkers import chk_string, chk_type -from device.service.driver_api._Driver import _Driver, RESOURCE_SERVICES +from device.service.driver_api._Driver import _Driver, RESOURCE_ENDPOINTS, RESOURCE_SERVICES from .handlers.EthtServiceHandler import EthtServiceHandler from .handlers.OsuTunnelHandler import OsuTunnelHandler from .handlers.RestApiClient import RestApiClient @@ -25,6 +25,7 @@ from .Tools import get_etht_services, get_osu_tunnels, parse_resource_key LOGGER = logging.getLogger(__name__) ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, RESOURCE_SERVICES, ] @@ -78,7 +79,12 @@ class IetfActnDriver(_Driver): try: _results = list() - if resource_key == RESOURCE_SERVICES: + if resource_key == RESOURCE_ENDPOINTS: + # Add mgmt endpoint by default + resource_key = '/endpoints/endpoint[mgmt]' + resource_value = {'uuid': 'mgmt', 'name': 'mgmt', 'type': 'mgmt'} + results.append((resource_key, resource_value)) + elif resource_key == RESOURCE_SERVICES: get_osu_tunnels(self._handler_osu_tunnel, _results) get_etht_services(self._handler_etht_service, _results) else: -- GitLab From 09151ece9abceb483f6effd19e8bdf01175239b1 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 26 Jan 2024 18:50:43 +0000 Subject: [PATCH 04/19] Manifests: - Activated DEBUG in device, service and pathcomp --- manifests/deviceservice.yaml | 2 +- manifests/pathcompservice.yaml | 4 ++-- manifests/serviceservice.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/manifests/deviceservice.yaml b/manifests/deviceservice.yaml index 77e421f29..7f7885daf 100644 --- a/manifests/deviceservice.yaml +++ b/manifests/deviceservice.yaml @@ -39,7 +39,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" startupProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:2020"] diff --git a/manifests/pathcompservice.yaml b/manifests/pathcompservice.yaml index 87d907a72..0ebd1811b 100644 --- a/manifests/pathcompservice.yaml +++ b/manifests/pathcompservice.yaml @@ -36,9 +36,9 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" - name: ENABLE_FORECASTER - value: "YES" + value: "NO" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:10020"] diff --git a/manifests/serviceservice.yaml b/manifests/serviceservice.yaml index 7d7bdaa4e..3865fd6c0 100644 --- a/manifests/serviceservice.yaml +++ b/manifests/serviceservice.yaml @@ -36,7 +36,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:3030"] -- GitLab From 5458a0bacec226e4100d0d48bcb38e8324476ed7 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 11:53:38 +0000 Subject: [PATCH 05/19] Manifests: - Activated DEBUG in nbi webui --- manifests/nbiservice.yaml | 2 +- manifests/webuiservice.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index de97ba364..f5477aeb4 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -37,7 +37,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:9090"] diff --git a/manifests/webuiservice.yaml b/manifests/webuiservice.yaml index 43caa9f04..89de36fc5 100644 --- a/manifests/webuiservice.yaml +++ b/manifests/webuiservice.yaml @@ -39,7 +39,7 @@ spec: - containerPort: 8004 env: - name: LOG_LEVEL - value: "INFO" + value: "DEBUG" - name: WEBUISERVICE_SERVICE_BASEURL_HTTP value: "/webui/" readinessProbe: -- GitLab From b4c94e13090f5786e9bd468dbca03192adfbbe15 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 11:55:50 +0000 Subject: [PATCH 06/19] WebUI component: - Added IP SDN controller icon --- .../static/topology_icons/Acknowledgements.txt | 8 ++++++-- .../topology_icons/emu-ip-sdn-controller.png | Bin 0 -> 10645 bytes .../static/topology_icons/ip-sdn-controller.png | Bin 0 -> 15075 bytes 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/webui/service/static/topology_icons/emu-ip-sdn-controller.png create mode 100644 src/webui/service/static/topology_icons/ip-sdn-controller.png diff --git a/src/webui/service/static/topology_icons/Acknowledgements.txt b/src/webui/service/static/topology_icons/Acknowledgements.txt index 43ecee798..08e9ed27c 100644 --- a/src/webui/service/static/topology_icons/Acknowledgements.txt +++ b/src/webui/service/static/topology_icons/Acknowledgements.txt @@ -31,5 +31,9 @@ https://symbols.getvecta.com/stencil_241/158_local-director.6b38eab9e4.png => em https://symbols.getvecta.com/stencil_240/197_radio-tower.b6138c8c29.png => radio-router.png https://symbols.getvecta.com/stencil_241/216_radio-tower.5159339bc0.png => emu-radio-router.png -https://symbols.getvecta.com/stencil_240/124_laptop.be264ceb77.png => laptop.png -https://symbols.getvecta.com/stencil_241/154_laptop.c01910b6c8.png => emu-laptop.png +https://symbols.getvecta.com/stencil_240/124_laptop.be264ceb77.png => client.png +https://symbols.getvecta.com/stencil_241/154_laptop.c01910b6c8.png => emu-client.png + +https://symbols.getvecta.com/stencil_240/16_atm-tag-switch-router.3149d7e933.png => ip-sdn-controller.png +https://symbols.getvecta.com/stencil_241/46_atm-tag-sw-rtr.776719c0b0.png => emu-ip-sdn-controller.png + diff --git a/src/webui/service/static/topology_icons/emu-ip-sdn-controller.png b/src/webui/service/static/topology_icons/emu-ip-sdn-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4c69120e28b434df3d5a8db46fe304dbdd03e7 GIT binary patch literal 10645 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 DJ1o6o literal 0 HcmV?d00001 diff --git a/src/webui/service/static/topology_icons/ip-sdn-controller.png b/src/webui/service/static/topology_icons/ip-sdn-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b1abe8738b7e34faaa34d286157a778069fdbb GIT binary patch literal 15075 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 Date: Tue, 30 Jan 2024 11:57:56 +0000 Subject: [PATCH 07/19] Common - Tools - Descriptor: - Added device type IP SDN Controller - Added logic to split controllers and devices - Controllers are onboarded first, then devices --- src/common/DeviceTypes.py | 2 ++ src/common/tools/descriptor/Loader.py | 44 +++++++++++++++------------ src/common/tools/descriptor/Tools.py | 22 ++++++++++++++ 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index f88f931d4..72b3e21fd 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -22,6 +22,7 @@ class DeviceTypeEnum(Enum): # Emulated device types EMULATED_CLIENT = 'emu-client' EMULATED_DATACENTER = 'emu-datacenter' + EMULATED_IP_SDN_CONTROLLER = 'emu-ip-sdn-controller' EMULATED_MICROWAVE_RADIO_SYSTEM = 'emu-microwave-radio-system' EMULATED_OPEN_LINE_SYSTEM = 'emu-open-line-system' EMULATED_OPTICAL_ROADM = 'emu-optical-roadm' @@ -36,6 +37,7 @@ class DeviceTypeEnum(Enum): # Real device types CLIENT = 'client' DATACENTER = 'datacenter' + IP_SDN_CONTROLLER = 'ip-sdn-controller' MICROWAVE_RADIO_SYSTEM = 'microwave-radio-system' OPEN_LINE_SYSTEM = 'open-line-system' OPTICAL_ROADM = 'optical-roadm' diff --git a/src/common/tools/descriptor/Loader.py b/src/common/tools/descriptor/Loader.py index c5468c19c..11500a32b 100644 --- a/src/common/tools/descriptor/Loader.py +++ b/src/common/tools/descriptor/Loader.py @@ -46,7 +46,7 @@ from slice.client.SliceClient import SliceClient from .Tools import ( format_device_custom_config_rules, format_service_custom_config_rules, format_slice_custom_config_rules, get_descriptors_add_contexts, get_descriptors_add_services, get_descriptors_add_slices, - get_descriptors_add_topologies, split_devices_by_rules) + get_descriptors_add_topologies, split_controllers_and_network_devices, split_devices_by_rules) LOGGER = logging.getLogger(__name__) LOGGERS = { @@ -56,14 +56,15 @@ LOGGERS = { } ENTITY_TO_TEXT = { - # name => singular, plural - 'context' : ('Context', 'Contexts' ), - 'topology' : ('Topology', 'Topologies' ), - 'device' : ('Device', 'Devices' ), - 'link' : ('Link', 'Links' ), - 'service' : ('Service', 'Services' ), - 'slice' : ('Slice', 'Slices' ), - 'connection': ('Connection', 'Connections'), + # name => singular, plural + 'context' : ('Context', 'Contexts' ), + 'topology' : ('Topology', 'Topologies' ), + 'controller': ('Controller', 'Controllers' ), + 'device' : ('Device', 'Devices' ), + 'link' : ('Link', 'Links' ), + 'service' : ('Service', 'Services' ), + 'slice' : ('Slice', 'Slices' ), + 'connection': ('Connection', 'Connections' ), } ACTION_TO_TEXT = { @@ -231,10 +232,12 @@ class DescriptorLoader: def _load_dummy_mode(self) -> None: # Dummy Mode: used to pre-load databases (WebUI debugging purposes) with no smart or automated tasks. + controllers, network_devices = split_controllers_and_network_devices(self.__devices) self.__ctx_cli.connect() self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) - self._process_descr('device', 'add', self.__ctx_cli.SetDevice, Device, self.__devices ) + self._process_descr('controller', 'add', self.__ctx_cli.SetDevice, Device, controllers ) + self._process_descr('device', 'add', self.__ctx_cli.SetDevice, Device, network_devices ) self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) self._process_descr('service', 'add', self.__ctx_cli.SetService, Service, self.__services ) self._process_descr('slice', 'add', self.__ctx_cli.SetSlice, Slice, self.__slices ) @@ -262,20 +265,23 @@ class DescriptorLoader: self.__services_add = get_descriptors_add_services(self.__services) self.__slices_add = get_descriptors_add_slices(self.__slices) + controllers_add, network_devices_add = split_controllers_and_network_devices(self.__devices_add) + self.__ctx_cli.connect() self.__dev_cli.connect() self.__svc_cli.connect() self.__slc_cli.connect() - self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) - self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) - self._process_descr('device', 'add', self.__dev_cli.AddDevice, Device, self.__devices_add ) - self._process_descr('device', 'config', self.__dev_cli.ConfigureDevice, Device, self.__devices_config) - self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) - self._process_descr('service', 'add', self.__svc_cli.CreateService, Service, self.__services_add ) - self._process_descr('service', 'update', self.__svc_cli.UpdateService, Service, self.__services ) - self._process_descr('slice', 'add', self.__slc_cli.CreateSlice, Slice, self.__slices_add ) - self._process_descr('slice', 'update', self.__slc_cli.UpdateSlice, Slice, self.__slices ) + self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) + self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) + self._process_descr('controller', 'add', self.__dev_cli.AddDevice, Device, controllers_add ) + self._process_descr('device', 'add', self.__dev_cli.AddDevice, Device, network_devices_add ) + self._process_descr('device', 'config', self.__dev_cli.ConfigureDevice, Device, self.__devices_config) + self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) + self._process_descr('service', 'add', self.__svc_cli.CreateService, Service, self.__services_add ) + self._process_descr('service', 'update', self.__svc_cli.UpdateService, Service, self.__services ) + self._process_descr('slice', 'add', self.__slc_cli.CreateSlice, Slice, self.__slices_add ) + self._process_descr('slice', 'update', self.__slc_cli.UpdateSlice, Slice, self.__slices ) # By default the Context component automatically assigns devices and links to topologies based on their # endpoints, and assigns topologies, services, and slices to contexts based on their identifiers. diff --git a/src/common/tools/descriptor/Tools.py b/src/common/tools/descriptor/Tools.py index 3126f2bce..b4a76ff4f 100644 --- a/src/common/tools/descriptor/Tools.py +++ b/src/common/tools/descriptor/Tools.py @@ -14,6 +14,7 @@ import copy, json from typing import Dict, List, Optional, Tuple, Union +from common.DeviceTypes import DeviceTypeEnum def get_descriptors_add_contexts(contexts : List[Dict]) -> List[Dict]: contexts_add = copy.deepcopy(contexts) @@ -103,3 +104,24 @@ def split_devices_by_rules(devices : List[Dict]) -> Tuple[List[Dict], List[Dict] devices_config.append(device) return devices_add, devices_config + +CONTROLLER_DEVICE_TYPES = { + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER.value, + DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value, + DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value, + DeviceTypeEnum.IP_SDN_CONTROLLER.value, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value, + DeviceTypeEnum.OPEN_LINE_SYSTEM.value, + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value, +} + +def split_controllers_and_network_devices(devices : List[Dict]) -> Tuple[List[Dict], List[Dict]]: + controllers : List[Dict] = list() + network_devices : List[Dict] = list() + for device in devices: + device_type = device.get('device_type') + if device_type in CONTROLLER_DEVICE_TYPES: + controllers.append(device) + else: + network_devices.append(device) + return controllers, network_devices -- GitLab From 222c46e1bcd32660eefd2aeaf68e1b9e61788b21 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 12:00:23 +0000 Subject: [PATCH 08/19] PathComp component - Frontend: - Added IP SDN Ctrl as a packet device in sub-service composer and resource groups - Added logic to separate connections managed by intermediate SDN controllers --- .../algorithms/tools/ComputeSubServices.py | 30 +++++++++++++++++++ .../algorithms/tools/ResourceGroups.py | 3 ++ .../service/algorithms/tools/ServiceTypes.py | 1 + 3 files changed, 34 insertions(+) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py index 06b24031b..86a91d00a 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py @@ -95,6 +95,36 @@ def convert_explicit_path_hops_to_connections( connections.append(connection) connection_stack.queue[-1][3].append(connection[0]) #connection_stack.queue[-1][2].append(path_hop) + elif prv_res_class[2] is None and res_class[2] is not None: + # entering domain of a device controller, create underlying connection + LOGGER.debug(' entering domain of a device controller, create underlying connection') + sub_service_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((sub_service_uuid, service_type, [path_hop], [])) + elif prv_res_class[2] is not None and res_class[2] is None: + # leaving domain of a device controller, terminate underlying connection + LOGGER.debug(' leaving domain of a device controller, terminate underlying connection') + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + connection_stack.queue[-1][2].append(path_hop) + elif prv_res_class[2] is not None and res_class[2] is not None: + if prv_res_class[2] == res_class[2]: + # stay in domain of a device controller, connection continues + LOGGER.debug(' stay in domain of a device controller, connection continues') + connection_stack.queue[-1][2].append(path_hop) + else: + # switching to different device controller, chain connections + LOGGER.debug(' switching to different device controller, chain connections') + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + + sub_service_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((sub_service_uuid, service_type, [path_hop], [])) elif prv_res_class[0] is None: # path ingress LOGGER.debug(' path ingress') diff --git a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py index 843c41803..7b5221c88 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py @@ -24,6 +24,9 @@ DEVICE_TYPE_TO_DEEPNESS = { DeviceTypeEnum.DATACENTER.value : 90, DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER.value : 80, + DeviceTypeEnum.IP_SDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value : 70, DeviceTypeEnum.PACKET_ROUTER.value : 70, diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py index 73a741ae5..094baa1a6 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -22,6 +22,7 @@ NETWORK_DEVICE_TYPES = { PACKET_DEVICE_TYPES = { DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + DeviceTypeEnum.IP_SDN_CONTROLLER, DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER, DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER, DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, } -- GitLab From f7806b29af0499e2d18bdcf1ad20c32e37ecddd2 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 12:00:59 +0000 Subject: [PATCH 09/19] Device component: - Added logic to store in Context the explicit controller of a device --- src/device/service/DeviceServiceServicerImpl.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py index eeffdd7b0..3df7c4822 100644 --- a/src/device/service/DeviceServiceServicerImpl.py +++ b/src/device/service/DeviceServiceServicerImpl.py @@ -73,6 +73,13 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): device.device_operational_status = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED device.device_drivers.extend(request.device_drivers) # pylint: disable=no-member device.device_config.CopyFrom(request.device_config) # pylint: disable=no-member + + if request.HasField('controller_id'): + controller_id = request.controller_id + if controller_id.HasField('device_uuid'): + controller_device_uuid = controller_id.device_uuid.uuid + device.controller_id.device_uuid.uuid = controller_device_uuid + device_id = context_client.SetDevice(device) device = get_device(context_client, device_id.device_uuid.uuid, rw_copy=True) -- GitLab From 8882ddab1c61aadf5fddf10cac34e0a260db1af6 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 12:18:16 +0000 Subject: [PATCH 10/19] Device component - IETF ACTN: - Corrected unitary test data files --- src/device/tests/data/ietf_actn/config_rules.json | 2 +- src/device/tests/data/ietf_actn/expected_etht_services.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/device/tests/data/ietf_actn/config_rules.json b/src/device/tests/data/ietf_actn/config_rules.json index d73a68674..d106a5a8f 100644 --- a/src/device/tests/data/ietf_actn/config_rules.json +++ b/src/device/tests/data/ietf_actn/config_rules.json @@ -26,7 +26,7 @@ ["128.32.20.5", 24, "128.32.33.5"] ], "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_vlan_tag": 201, "dst_static_routes": [ - ["172.1.101.22", 24, "172.10.33.5"] + ["172.1.201.22", 24, "172.10.33.5"] ] }}} ] diff --git a/src/device/tests/data/ietf_actn/expected_etht_services.json b/src/device/tests/data/ietf_actn/expected_etht_services.json index d9f410526..72c48e6b3 100644 --- a/src/device/tests/data/ietf_actn/expected_etht_services.json +++ b/src/device/tests/data/ietf_actn/expected_etht_services.json @@ -139,7 +139,7 @@ "is-terminal": true, "static-route-list": [ { - "destination": "172.1.101.22", + "destination": "172.1.201.22", "destination-mask": 24, "next-hop": "172.10.33.5" } -- GitLab From b2bc478e76bc25a2f0fbad2bc2939bb0f09ba4bb Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 17:15:45 +0000 Subject: [PATCH 11/19] Common - Tools - gRPC: - Extended method to manage Constraints to enable specifying a new action --- src/common/tools/grpc/Constraints.py | 52 +++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/common/tools/grpc/Constraints.py b/src/common/tools/grpc/Constraints.py index 07f0b7782..63e707c6f 100644 --- a/src/common/tools/grpc/Constraints.py +++ b/src/common/tools/grpc/Constraints.py @@ -18,11 +18,12 @@ import json from typing import Any, Dict, List, Optional, Tuple -from common.proto.context_pb2 import Constraint, EndPointId +from common.proto.context_pb2 import Constraint, ConstraintActionEnum, EndPointId from common.tools.grpc.Tools import grpc_message_to_json_string def update_constraint_custom_scalar( - constraints, constraint_type : str, value : Any, raise_if_differs : bool = False + constraints, constraint_type : str, value : Any, raise_if_differs : bool = False, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: for constraint in constraints: @@ -36,6 +37,8 @@ def update_constraint_custom_scalar( constraint.custom.constraint_type = constraint_type json_constraint_value = None + constraint.action = new_action + if (json_constraint_value is None) or not raise_if_differs: # missing or raise_if_differs=False, add/update it json_constraint_value = value @@ -47,7 +50,10 @@ def update_constraint_custom_scalar( constraint.custom.constraint_value = json.dumps(json_constraint_value, sort_keys=True) return constraint -def update_constraint_custom_dict(constraints, constraint_type : str, fields : Dict[str, Tuple[Any, bool]]) -> Constraint: +def update_constraint_custom_dict( + constraints, constraint_type : str, fields : Dict[str, Tuple[Any, bool]], + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: # fields: Dict[field_name : str, Tuple[field_value : Any, raise_if_differs : bool]] for constraint in constraints: @@ -61,6 +67,8 @@ def update_constraint_custom_dict(constraints, constraint_type : str, fields : D constraint.custom.constraint_type = constraint_type json_constraint_value = {} + constraint.action = new_action + for field_name,(field_value, raise_if_differs) in fields.items(): if (field_name not in json_constraint_value) or not raise_if_differs: # missing or raise_if_differs=False, add/update it @@ -75,7 +83,8 @@ def update_constraint_custom_dict(constraints, constraint_type : str, fields : D def update_constraint_endpoint_location( constraints, endpoint_id : EndPointId, - region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None + region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: # gps_position: (latitude, longitude) if region is not None and gps_position is not None: @@ -103,6 +112,8 @@ def update_constraint_endpoint_location( _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid + constraint.action = new_action + location = constraint.endpoint_location.location if region is not None: location.region = region @@ -111,7 +122,10 @@ def update_constraint_endpoint_location( location.gps_position.longitude = gps_position[1] return constraint -def update_constraint_endpoint_priority(constraints, endpoint_id : EndPointId, priority : int) -> Constraint: +def update_constraint_endpoint_priority( + constraints, endpoint_id : EndPointId, priority : int, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: endpoint_uuid = endpoint_id.endpoint_uuid.uuid device_uuid = endpoint_id.device_id.device_uuid.uuid topology_uuid = endpoint_id.topology_id.topology_uuid.uuid @@ -134,10 +148,15 @@ def update_constraint_endpoint_priority(constraints, endpoint_id : EndPointId, p _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid + constraint.action = new_action + constraint.endpoint_priority.priority = priority return constraint -def update_constraint_sla_capacity(constraints, capacity_gbps : float) -> Constraint: +def update_constraint_sla_capacity( + constraints, capacity_gbps : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_capacity': continue break # found, end loop @@ -145,10 +164,15 @@ def update_constraint_sla_capacity(constraints, capacity_gbps : float) -> Constr # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_capacity.capacity_gbps = capacity_gbps return constraint -def update_constraint_sla_latency(constraints, e2e_latency_ms : float) -> Constraint: +def update_constraint_sla_latency( + constraints, e2e_latency_ms : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_latency': continue break # found, end loop @@ -156,11 +180,14 @@ def update_constraint_sla_latency(constraints, e2e_latency_ms : float) -> Constr # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_latency.e2e_latency_ms = e2e_latency_ms return constraint def update_constraint_sla_availability( - constraints, num_disjoint_paths : int, all_active : bool, availability : float + constraints, num_disjoint_paths : int, all_active : bool, availability : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_availability': continue @@ -169,12 +196,17 @@ def update_constraint_sla_availability( # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_availability.num_disjoint_paths = num_disjoint_paths constraint.sla_availability.all_active = all_active constraint.sla_availability.availability = availability return constraint -def update_constraint_sla_isolation(constraints, isolation_levels : List[int]) -> Constraint: +def update_constraint_sla_isolation( + constraints, isolation_levels : List[int], + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_isolation': continue break # found, end loop @@ -182,6 +214,8 @@ def update_constraint_sla_isolation(constraints, isolation_levels : List[int]) - # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + for isolation_level in isolation_levels: if isolation_level in constraint.sla_isolation.isolation_level: continue constraint.sla_isolation.isolation_level.append(isolation_level) -- GitLab From 1ddd68dd9920a9c233c5184a0176c2aa5438fa66 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 17:16:50 +0000 Subject: [PATCH 12/19] NBI component - IETF L3VPN: - Added missing data files - Updated test files - Corrected config rules to store address of neighbor devices --- .../nbi_plugins/ietf_l3vpn/Handlers.py | 14 +- .../ietf_l3vpn/yang/ietf_l3vpn_tree.txt | 413 ++++++++++++++++++ src/nbi/tests/data/ietf_l3vpn_req_svc1.json | 12 +- src/nbi/tests/data/ietf_l3vpn_req_svc2.json | 12 +- 4 files changed, 433 insertions(+), 18 deletions(-) create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py index 2192ea942..3466c8598 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py @@ -55,7 +55,7 @@ def process_vpn_service( def update_service_endpoint( service_uuid : str, site_id : str, device_uuid : str, endpoint_uuid : str, - vlan_tag : int, ipv4_address : str, ipv4_prefix_length : int, + vlan_tag : int, ipv4_address : str, neighbor_ipv4_address : str, ipv4_prefix_length : int, capacity_gbps : Optional[float] = None, e2e_latency_ms : Optional[float] = None, availability : Optional[float] = None, mtu : Optional[int] = None, static_routing : Optional[Dict[Tuple[str, str], str]] = None, @@ -94,9 +94,10 @@ def update_service_endpoint( ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, vlan_tag) field_updates = {} - if vlan_tag is not None: field_updates['vlan_tag' ] = (vlan_tag, True) - if ipv4_address is not None: field_updates['ip_address' ] = (ipv4_address, True) - if ipv4_prefix_length is not None: field_updates['prefix_length'] = (ipv4_prefix_length, True) + if vlan_tag is not None: field_updates['vlan_tag' ] = (vlan_tag, True) + if ipv4_address is not None: field_updates['ip_address' ] = (ipv4_address, True) + if neighbor_ipv4_address is not None: field_updates['neighbor_address'] = (neighbor_ipv4_address, True) + if ipv4_prefix_length is not None: field_updates['prefix_length' ] = (ipv4_prefix_length, True) update_config_rule_custom(config_rules, endpoint_settings_key, field_updates) try: @@ -131,7 +132,7 @@ def process_site_network_access( raise NotImplementedError(MSG.format(str(ipv4_allocation['address-allocation-type']))) ipv4_allocation_addresses = ipv4_allocation['addresses'] ipv4_provider_address = ipv4_allocation_addresses['provider-address'] - #ipv4_customer_address = ipv4_allocation_addresses['customer-address'] + ipv4_customer_address = ipv4_allocation_addresses['customer-address'] ipv4_prefix_length = ipv4_allocation_addresses['prefix-length' ] vlan_tag = None @@ -176,7 +177,8 @@ def process_site_network_access( availability = qos_profile_class['bandwidth']['guaranteed-bw-percent'] exc = update_service_endpoint( - service_uuid, site_id, device_uuid, endpoint_uuid, vlan_tag, ipv4_provider_address, ipv4_prefix_length, + service_uuid, site_id, device_uuid, endpoint_uuid, + vlan_tag, ipv4_customer_address, ipv4_provider_address, ipv4_prefix_length, capacity_gbps=service_bandwidth_gbps, e2e_latency_ms=max_e2e_latency_ms, availability=availability, mtu=service_mtu, static_routing=site_static_routing ) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt new file mode 100644 index 000000000..e811c7c1b --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt @@ -0,0 +1,413 @@ +module: ietf-l3vpn-svc + +--rw l3vpn-svc + +--rw vpn-profiles + | +--rw valid-provider-identifiers + | +--rw cloud-identifier* [id] {cloud-access}? + | | +--rw id string + | +--rw encryption-profile-identifier* [id] + | | +--rw id string + | +--rw qos-profile-identifier* [id] + | | +--rw id string + | +--rw bfd-profile-identifier* [id] + | +--rw id string + +--rw vpn-services + | +--rw vpn-service* [vpn-id] + | +--rw vpn-id svc-id + | +--rw customer-name? string + | +--rw vpn-service-topology? identityref + | +--rw cloud-accesses {cloud-access}? + | | +--rw cloud-access* [cloud-identifier] + | | +--rw cloud-identifier -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/cloud-identifier/id + | | +--rw (list-flavor)? + | | | +--:(permit-any) + | | | | +--rw permit-any? empty + | | | +--:(deny-any-except) + | | | | +--rw permit-site* -> /l3vpn-svc/sites/site/site-id + | | | +--:(permit-any-except) + | | | +--rw deny-site* -> /l3vpn-svc/sites/site/site-id + | | +--rw address-translation + | | +--rw nat44 + | | +--rw enabled? boolean + | | +--rw nat44-customer-address? inet:ipv4-address + | +--rw multicast {multicast}? + | | +--rw enabled? boolean + | | +--rw customer-tree-flavors + | | | +--rw tree-flavor* identityref + | | +--rw rp + | | +--rw rp-group-mappings + | | | +--rw rp-group-mapping* [id] + | | | +--rw id uint16 + | | | +--rw provider-managed + | | | | +--rw enabled? boolean + | | | | +--rw rp-redundancy? boolean + | | | | +--rw optimal-traffic-delivery? boolean + | | | +--rw rp-address inet:ip-address + | | | +--rw groups + | | | +--rw group* [id] + | | | +--rw id uint16 + | | | +--rw (group-format) + | | | +--:(singleaddress) + | | | | +--rw group-address? inet:ip-address + | | | +--:(startend) + | | | +--rw group-start? inet:ip-address + | | | +--rw group-end? inet:ip-address + | | +--rw rp-discovery + | | +--rw rp-discovery-type? identityref + | | +--rw bsr-candidates + | | +--rw bsr-candidate-address* inet:ip-address + | +--rw carrierscarrier? boolean {carrierscarrier}? + | +--rw extranet-vpns {extranet-vpn}? + | +--rw extranet-vpn* [vpn-id] + | +--rw vpn-id svc-id + | +--rw local-sites-role? identityref + +--rw sites + +--rw site* [site-id] + +--rw site-id svc-id + +--rw requested-site-start? yang:date-and-time + +--rw requested-site-stop? yang:date-and-time + +--rw locations + | +--rw location* [location-id] + | +--rw location-id svc-id + | +--rw address? string + | +--rw postal-code? string + | +--rw state? string + | +--rw city? string + | +--rw country-code? string + +--rw devices + | +--rw device* [device-id] + | +--rw device-id svc-id + | +--rw location -> ../../../locations/location/location-id + | +--rw management + | +--rw address-family? address-family + | +--rw address inet:ip-address + +--rw site-diversity {site-diversity}? + | +--rw groups + | +--rw group* [group-id] + | +--rw group-id string + +--rw management + | +--rw type identityref + +--rw vpn-policies + | +--rw vpn-policy* [vpn-policy-id] + | +--rw vpn-policy-id svc-id + | +--rw entries* [id] + | +--rw id svc-id + | +--rw filters + | | +--rw filter* [type] + | | +--rw type identityref + | | +--rw lan-tag* string {lan-tag}? + | | +--rw ipv4-lan-prefix* inet:ipv4-prefix {ipv4}? + | | +--rw ipv6-lan-prefix* inet:ipv6-prefix {ipv6}? + | +--rw vpn* [vpn-id] + | +--rw vpn-id -> /l3vpn-svc/vpn-services/vpn-service/vpn-id + | +--rw site-role? identityref + +--rw site-vpn-flavor? identityref + +--rw maximum-routes + | +--rw address-family* [af] + | +--rw af address-family + | +--rw maximum-routes? uint32 + +--rw security + | +--rw authentication + | +--rw encryption {encryption}? + | +--rw enabled? boolean + | +--rw layer? enumeration + | +--rw encryption-profile + | +--rw (profile)? + | +--:(provider-profile) + | | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/encryption-profile-identifier/id + | +--:(customer-profile) + | +--rw algorithm? string + | +--rw (key-type)? + | +--:(psk) + | +--rw preshared-key? string + +--rw service + | +--rw qos {qos}? + | | +--rw qos-classification-policy + | | | +--rw rule* [id] + | | | +--rw id string + | | | +--rw (match-type)? + | | | | +--:(match-flow) + | | | | | +--rw match-flow + | | | | | +--rw dscp? inet:dscp + | | | | | +--rw dot1p? uint8 + | | | | | +--rw ipv4-src-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-src-prefix? inet:ipv6-prefix + | | | | | +--rw ipv4-dst-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-dst-prefix? inet:ipv6-prefix + | | | | | +--rw l4-src-port? inet:port-number + | | | | | +--rw target-sites* svc-id {target-sites}? + | | | | | +--rw l4-src-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw l4-dst-port? inet:port-number + | | | | | +--rw l4-dst-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw protocol-field? union + | | | | +--:(match-application) + | | | | +--rw match-application? identityref + | | | +--rw target-class-id? string + | | +--rw qos-profile + | | +--rw (qos-profile)? + | | +--:(standard) + | | | +--rw profile? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/qos-profile-identifier/id + | | +--:(custom) + | | +--rw classes {qos-custom}? + | | +--rw class* [class-id] + | | +--rw class-id string + | | +--rw direction? identityref + | | +--rw rate-limit? decimal64 + | | +--rw latency + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-latency? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint16 + | | +--rw jitter + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-jitter? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint32 + | | +--rw bandwidth + | | +--rw guaranteed-bw-percent decimal64 + | | +--rw end-to-end? empty + | +--rw carrierscarrier {carrierscarrier}? + | | +--rw signalling-type? enumeration + | +--rw multicast {multicast}? + | +--rw multicast-site-type? enumeration + | +--rw multicast-address-family + | | +--rw ipv4? boolean {ipv4}? + | | +--rw ipv6? boolean {ipv6}? + | +--rw protocol-type? enumeration + +--rw traffic-protection {fast-reroute}? + | +--rw enabled? boolean + +--rw routing-protocols + | +--rw routing-protocol* [type] + | +--rw type identityref + | +--rw ospf {rtg-ospf}? + | | +--rw address-family* address-family + | | +--rw area-address yang:dotted-quad + | | +--rw metric? uint16 + | | +--rw sham-links {rtg-ospf-sham-link}? + | | +--rw sham-link* [target-site] + | | +--rw target-site svc-id + | | +--rw metric? uint16 + | +--rw bgp {rtg-bgp}? + | | +--rw autonomous-system uint32 + | | +--rw address-family* address-family + | +--rw static + | | +--rw cascaded-lan-prefixes + | | +--rw ipv4-lan-prefixes* [lan next-hop] {ipv4}? + | | | +--rw lan inet:ipv4-prefix + | | | +--rw next-hop inet:ipv4-address + | | | +--rw lan-tag? string + | | +--rw ipv6-lan-prefixes* [lan next-hop] {ipv6}? + | | +--rw lan inet:ipv6-prefix + | | +--rw next-hop inet:ipv6-address + | | +--rw lan-tag? string + | +--rw rip {rtg-rip}? + | | +--rw address-family* address-family + | +--rw vrrp {rtg-vrrp}? + | +--rw address-family* address-family + +--ro actual-site-start? yang:date-and-time + +--ro actual-site-stop? yang:date-and-time + +--rw site-network-accesses + +--rw site-network-access* [site-network-access-id] + +--rw site-network-access-id svc-id + +--rw site-network-access-type? identityref + +--rw (location-flavor) + | +--:(location) + | | +--rw location-reference? -> ../../../locations/location/location-id + | +--:(device) + | +--rw device-reference? -> ../../../devices/device/device-id + +--rw access-diversity {site-diversity}? + | +--rw groups + | | +--rw group* [group-id] + | | +--rw group-id string + | +--rw constraints + | +--rw constraint* [constraint-type] + | +--rw constraint-type identityref + | +--rw target + | +--rw (target-flavor)? + | +--:(id) + | | +--rw group* [group-id] + | | +--rw group-id string + | +--:(all-accesses) + | | +--rw all-other-accesses? empty + | +--:(all-groups) + | +--rw all-other-groups? empty + +--rw bearer + | +--rw requested-type {requested-type}? + | | +--rw requested-type? string + | | +--rw strict? boolean + | +--rw always-on? boolean {always-on}? + | +--rw bearer-reference? string {bearer-reference}? + +--rw ip-connection + | +--rw ipv4 {ipv4}? + | | +--rw address-allocation-type? identityref + | | +--rw provider-dhcp + | | | +--rw provider-address? inet:ipv4-address + | | | +--rw prefix-length? uint8 + | | | +--rw (address-assign)? + | | | +--:(number) + | | | | +--rw number-of-dynamic-address? uint16 + | | | +--:(explicit) + | | | +--rw customer-addresses + | | | +--rw address-group* [group-id] + | | | +--rw group-id string + | | | +--rw start-address? inet:ipv4-address + | | | +--rw end-address? inet:ipv4-address + | | +--rw dhcp-relay + | | | +--rw provider-address? inet:ipv4-address + | | | +--rw prefix-length? uint8 + | | | +--rw customer-dhcp-servers + | | | +--rw server-ip-address* inet:ipv4-address + | | +--rw addresses + | | +--rw provider-address? inet:ipv4-address + | | +--rw customer-address? inet:ipv4-address + | | +--rw prefix-length? uint8 + | +--rw ipv6 {ipv6}? + | | +--rw address-allocation-type? identityref + | | +--rw provider-dhcp + | | | +--rw provider-address? inet:ipv6-address + | | | +--rw prefix-length? uint8 + | | | +--rw (address-assign)? + | | | +--:(number) + | | | | +--rw number-of-dynamic-address? uint16 + | | | +--:(explicit) + | | | +--rw customer-addresses + | | | +--rw address-group* [group-id] + | | | +--rw group-id string + | | | +--rw start-address? inet:ipv6-address + | | | +--rw end-address? inet:ipv6-address + | | +--rw dhcp-relay + | | | +--rw provider-address? inet:ipv6-address + | | | +--rw prefix-length? uint8 + | | | +--rw customer-dhcp-servers + | | | +--rw server-ip-address* inet:ipv6-address + | | +--rw addresses + | | +--rw provider-address? inet:ipv6-address + | | +--rw customer-address? inet:ipv6-address + | | +--rw prefix-length? uint8 + | +--rw oam + | +--rw bfd {bfd}? + | +--rw enabled? boolean + | +--rw (holdtime)? + | +--:(fixed) + | | +--rw fixed-value? uint32 + | +--:(profile) + | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/bfd-profile-identifier/id + +--rw security + | +--rw authentication + | +--rw encryption {encryption}? + | +--rw enabled? boolean + | +--rw layer? enumeration + | +--rw encryption-profile + | +--rw (profile)? + | +--:(provider-profile) + | | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/encryption-profile-identifier/id + | +--:(customer-profile) + | +--rw algorithm? string + | +--rw (key-type)? + | +--:(psk) + | +--rw preshared-key? string + +--rw service + | +--rw svc-input-bandwidth uint64 + | +--rw svc-output-bandwidth uint64 + | +--rw svc-mtu uint16 + | +--rw qos {qos}? + | | +--rw qos-classification-policy + | | | +--rw rule* [id] + | | | +--rw id string + | | | +--rw (match-type)? + | | | | +--:(match-flow) + | | | | | +--rw match-flow + | | | | | +--rw dscp? inet:dscp + | | | | | +--rw dot1p? uint8 + | | | | | +--rw ipv4-src-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-src-prefix? inet:ipv6-prefix + | | | | | +--rw ipv4-dst-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-dst-prefix? inet:ipv6-prefix + | | | | | +--rw l4-src-port? inet:port-number + | | | | | +--rw target-sites* svc-id {target-sites}? + | | | | | +--rw l4-src-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw l4-dst-port? inet:port-number + | | | | | +--rw l4-dst-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw protocol-field? union + | | | | +--:(match-application) + | | | | +--rw match-application? identityref + | | | +--rw target-class-id? string + | | +--rw qos-profile + | | +--rw (qos-profile)? + | | +--:(standard) + | | | +--rw profile? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/qos-profile-identifier/id + | | +--:(custom) + | | +--rw classes {qos-custom}? + | | +--rw class* [class-id] + | | +--rw class-id string + | | +--rw direction? identityref + | | +--rw rate-limit? decimal64 + | | +--rw latency + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-latency? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint16 + | | +--rw jitter + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-jitter? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint32 + | | +--rw bandwidth + | | +--rw guaranteed-bw-percent decimal64 + | | +--rw end-to-end? empty + | +--rw carrierscarrier {carrierscarrier}? + | | +--rw signalling-type? enumeration + | +--rw multicast {multicast}? + | +--rw multicast-site-type? enumeration + | +--rw multicast-address-family + | | +--rw ipv4? boolean {ipv4}? + | | +--rw ipv6? boolean {ipv6}? + | +--rw protocol-type? enumeration + +--rw routing-protocols + | +--rw routing-protocol* [type] + | +--rw type identityref + | +--rw ospf {rtg-ospf}? + | | +--rw address-family* address-family + | | +--rw area-address yang:dotted-quad + | | +--rw metric? uint16 + | | +--rw sham-links {rtg-ospf-sham-link}? + | | +--rw sham-link* [target-site] + | | +--rw target-site svc-id + | | +--rw metric? uint16 + | +--rw bgp {rtg-bgp}? + | | +--rw autonomous-system uint32 + | | +--rw address-family* address-family + | +--rw static + | | +--rw cascaded-lan-prefixes + | | +--rw ipv4-lan-prefixes* [lan next-hop] {ipv4}? + | | | +--rw lan inet:ipv4-prefix + | | | +--rw next-hop inet:ipv4-address + | | | +--rw lan-tag? string + | | +--rw ipv6-lan-prefixes* [lan next-hop] {ipv6}? + | | +--rw lan inet:ipv6-prefix + | | +--rw next-hop inet:ipv6-address + | | +--rw lan-tag? string + | +--rw rip {rtg-rip}? + | | +--rw address-family* address-family + | +--rw vrrp {rtg-vrrp}? + | +--rw address-family* address-family + +--rw availability + | +--rw access-priority? uint32 + +--rw vpn-attachment + +--rw (attachment-flavor) + +--:(vpn-policy-id) + | +--rw vpn-policy-id? -> ../../../../vpn-policies/vpn-policy/vpn-policy-id + +--:(vpn-id) + +--rw vpn-id? -> /l3vpn-svc/vpn-services/vpn-service/vpn-id + +--rw site-role? identityref diff --git a/src/nbi/tests/data/ietf_l3vpn_req_svc1.json b/src/nbi/tests/data/ietf_l3vpn_req_svc1.json index 66e253cb5..bfeb93fb7 100644 --- a/src/nbi/tests/data/ietf_l3vpn_req_svc1.json +++ b/src/nbi/tests/data/ietf_l3vpn_req_svc1.json @@ -39,12 +39,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan21", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan21", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" } ] } @@ -82,7 +82,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan21", - "next-hop": "10.0.10.1" + "next-hop": "128.32.33.254" } ] } @@ -147,7 +147,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan101", - "next-hop": "172.10.33.5" + "next-hop": "172.10.33.2" } ] } @@ -185,12 +185,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan101", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan101", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" } ] } diff --git a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json index 2d2ea2c22..4ecf3c2ea 100644 --- a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json +++ b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json @@ -39,12 +39,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan31", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan31", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" } ] } @@ -82,7 +82,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan31", - "next-hop": "10.0.10.1" + "next-hop": "128.32.33.254" } ] } @@ -147,7 +147,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan201", - "next-hop": "172.10.33.1" + "next-hop": "172.10.33.2" } ] } @@ -185,12 +185,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan201", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan201", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" } ] } -- GitLab From 538582a7481b629b4f9223edd8873eca5d7d2fa2 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 30 Jan 2024 17:18:11 +0000 Subject: [PATCH 13/19] PathComp component - Front-end: - Extended ComposeConfigRules to propagate static routes in L3 services - Extended ComposeConfigRules to recognize custom config rules with the form /device[]/endpoint[]/vlan[]/settings --- .../algorithms/tools/ComposeConfigRules.py | 76 +++++++++++++------ 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 329552a91..e58a264e1 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -21,19 +21,21 @@ from common.tools.object_factory.ConfigRule import json_config_rule_set LOGGER = logging.getLogger(__name__) SETTINGS_RULE_NAME = '/settings' +STATIC_ROUTING_RULE_NAME = '/static_routing' -DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') -ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') +RE_DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') +RE_ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') +RE_ENDPOINT_VLAN_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/vlan\[([^\]]+)\]\/settings') L2NM_SETTINGS_FIELD_DEFAULTS = { - 'encapsulation_type': 'dot1q', - 'vlan_id' : 100, + #'encapsulation_type': 'dot1q', + #'vlan_id' : 100, 'mtu' : 1450, } L3NM_SETTINGS_FIELD_DEFAULTS = { - 'encapsulation_type': 'dot1q', - 'vlan_id' : 100, + #'encapsulation_type': 'dot1q', + #'vlan_id' : 100, 'mtu' : 1450, } @@ -54,26 +56,48 @@ def find_custom_config_rule(config_rules : List, resource_name : str) -> Optiona return resource_value def compose_config_rules( - main_service_config_rules : List, subservice_config_rules : List, field_defaults : Dict + main_service_config_rules : List, subservice_config_rules : List, settings_rule_name : str, field_defaults : Dict ) -> None: - settings = find_custom_config_rule(main_service_config_rules, SETTINGS_RULE_NAME) + settings = find_custom_config_rule(main_service_config_rules, settings_rule_name) if settings is None: return json_settings = {} - for field_name,default_value in field_defaults.items(): - json_settings[field_name] = settings.get(field_name, default_value) - config_rule = ConfigRule(**json_config_rule_set('/settings', json_settings)) + if len(field_defaults) == 0: + for field_name,field_value in settings.items(): + json_settings[field_name] = field_value + else: + for field_name,default_value in field_defaults.items(): + field_value = settings.get(field_name, default_value) + if field_value is None: continue + json_settings[field_name] = field_value + + if len(json_settings) == 0: return + + config_rule = ConfigRule(**json_config_rule_set(settings_rule_name, json_settings)) subservice_config_rules.append(config_rule) def compose_l2nm_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: - compose_config_rules(main_service_config_rules, subservice_config_rules, L2NM_SETTINGS_FIELD_DEFAULTS) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, L2NM_SETTINGS_FIELD_DEFAULTS), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) def compose_l3nm_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: - compose_config_rules(main_service_config_rules, subservice_config_rules, L3NM_SETTINGS_FIELD_DEFAULTS) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, L3NM_SETTINGS_FIELD_DEFAULTS), + (STATIC_ROUTING_RULE_NAME, {}), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) 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) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, TAPI_SETTINGS_FIELD_DEFAULTS), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) def compose_device_config_rules( config_rules : List, subservice_config_rules : List, path_hops : List, @@ -127,25 +151,31 @@ def compose_device_config_rules( elif config_rule.WhichOneof('config_rule') == 'custom': LOGGER.debug('[compose_device_config_rules] is custom') - match = DEVICE_SETTINGS.match(config_rule.custom.resource_key) + match = RE_DEVICE_SETTINGS.match(config_rule.custom.resource_key) if match is not None: 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} + device_keys = {'?', device_uuid_or_name} + device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) + if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) if len(device_keys.intersection(devices_traversed)) == 0: continue subservice_config_rules.append(config_rule) - match = ENDPOINT_SETTINGS.match(config_rule.custom.resource_key) + match = RE_ENDPOINT_SETTINGS.match(config_rule.custom.resource_key) + if match is None: + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule.custom.resource_key) if match is not None: 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} + device_keys = {'?', device_uuid_or_name} + device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) + if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) endpoint_uuid_or_name = match.group(2) - 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} + endpoint_keys = {'?', endpoint_uuid_or_name} + endpoint_name_or_uuid_1 = endpoint_name_mapping.get((device_uuid_or_name, endpoint_uuid_or_name)) + if endpoint_name_or_uuid_1 is not None: endpoint_keys.add(endpoint_name_or_uuid_1) + endpoint_name_or_uuid_2 = endpoint_name_mapping.get((device_name_or_uuid, endpoint_uuid_or_name)) + if endpoint_name_or_uuid_2 is not None: endpoint_keys.add(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 -- GitLab From 022cd29d875f5c0abb414665c2f284301e2e10ef Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 1 Feb 2024 16:47:40 +0000 Subject: [PATCH 14/19] PathComp component - Front-end: - Added method generate_neighbor_endpoint_config_rules to compose config rules for neighbor endpoints --- .../frontend/service/algorithms/_Algorithm.py | 23 ++- .../algorithms/tools/ComposeConfigRules.py | 151 +++++++++++++++++- 2 files changed, 169 insertions(+), 5 deletions(-) diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 0a1b62040..ca9783108 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -15,12 +15,16 @@ import json, logging, requests, uuid from typing import Dict, List, Optional, Tuple, Union from common.proto.context_pb2 import ( - Connection, Device, DeviceList, EndPointId, Link, LinkList, Service, ServiceStatusEnum, ServiceTypeEnum) + ConfigRule, Connection, Device, DeviceList, EndPointId, Link, LinkList, Service, ServiceStatusEnum, ServiceTypeEnum +) from common.proto.pathcomp_pb2 import PathCompReply, PathCompRequest +from common.tools.grpc.Tools import grpc_message_list_to_json from pathcomp.frontend.Config import BACKEND_URL from .tools.EroPathToHops import eropath_to_hops from .tools.ComposeConfigRules import ( - compose_device_config_rules, compose_l2nm_config_rules, compose_l3nm_config_rules, compose_tapi_config_rules) + compose_device_config_rules, compose_l2nm_config_rules, compose_l3nm_config_rules, compose_tapi_config_rules, + generate_neighbor_endpoint_config_rules +) 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) @@ -227,12 +231,25 @@ class _Algorithm: continue orig_config_rules = grpc_orig_service.service_config.config_rules + json_orig_config_rules = grpc_message_list_to_json(orig_config_rules) for service_path_ero in response['path']: self.logger.debug('service_path_ero["devices"] = {:s}'.format(str(service_path_ero['devices']))) _endpoint_to_link_dict = {k:v[0] for k,v in self.endpoint_to_link_dict.items()} self.logger.debug('self.endpoint_to_link_dict = {:s}'.format(str(_endpoint_to_link_dict))) path_hops = eropath_to_hops(service_path_ero['devices'], self.endpoint_to_link_dict) + + json_generated_config_rules = generate_neighbor_endpoint_config_rules( + json_orig_config_rules, path_hops, self.device_name_mapping, self.endpoint_name_mapping + ) + json_extended_config_rules = list() + json_extended_config_rules.extend(json_orig_config_rules) + json_extended_config_rules.extend(json_generated_config_rules) + extended_config_rules = [ + ConfigRule(**json_extended_config_rule) + for json_extended_config_rule in json_extended_config_rules + ] + self.logger.debug('path_hops = {:s}'.format(str(path_hops))) try: _device_dict = {k:v[0] for k,v in self.device_dict.items()} @@ -256,7 +273,7 @@ class _Algorithm: if service_key in grpc_services: continue grpc_service = self.add_service_to_reply( reply, context_uuid, service_uuid, service_type, path_hops=path_hops, - config_rules=orig_config_rules) + config_rules=extended_config_rules) grpc_services[service_key] = grpc_service for connection in connections: diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index e58a264e1..8e99a1ae1 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 itertools, json, logging, re -from typing import Dict, List, Optional, Tuple +import copy, itertools, json, logging, re +from typing import Dict, Iterable, List, Optional, Set, Tuple from common.proto.context_pb2 import ConfigRule from common.tools.grpc.Tools import grpc_message_to_json_string from common.tools.object_factory.ConfigRule import json_config_rule_set @@ -23,10 +23,15 @@ LOGGER = logging.getLogger(__name__) SETTINGS_RULE_NAME = '/settings' STATIC_ROUTING_RULE_NAME = '/static_routing' +RE_UUID = re.compile(r'([0-9a-fA-F]{8})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{12})') + RE_DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') RE_ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') RE_ENDPOINT_VLAN_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/vlan\[([^\]]+)\]\/settings') +TMPL_ENDPOINT_SETTINGS = '/device[{:s}]/endpoint[{:s}]/settings' +TMPL_ENDPOINT_VLAN_SETTINGS = '/device[{:s}]/endpoint[{:s}]/vlan[{:s}]/settings' + L2NM_SETTINGS_FIELD_DEFAULTS = { #'encapsulation_type': 'dot1q', #'vlan_id' : 100, @@ -183,4 +188,146 @@ def compose_device_config_rules( else: continue + for config_rule in subservice_config_rules: + LOGGER.debug('[compose_device_config_rules] result config_rule: {:s}'.format( + grpc_message_to_json_string(config_rule))) + LOGGER.debug('[compose_device_config_rules] end') + +def pairwise(iterable : Iterable) -> Tuple[Iterable, Iterable]: + # TODO: To be replaced by itertools.pairwise() when we move to Python 3.10 + # Python 3.10 introduced method itertools.pairwise() + # Standalone method extracted from: + # - https://docs.python.org/3/library/itertools.html#itertools.pairwise + a, b = itertools.tee(iterable, 2) + next(b, None) + return zip(a, b) + +def compute_device_keys( + device_uuid_or_name : str, device_name_mapping : Dict[str, str] +) -> Set[str]: + LOGGER.debug('[compute_device_keys] begin') + LOGGER.debug('[compute_device_keys] device_uuid_or_name={:s}'.format(str(device_uuid_or_name))) + #LOGGER.debug('[compute_device_keys] device_name_mapping={:s}'.format(str(device_name_mapping))) + + device_keys = {device_uuid_or_name} + for k,v in device_name_mapping.items(): + if device_uuid_or_name not in {k, v}: continue + device_keys.add(k) + device_keys.add(v) + + LOGGER.debug('[compute_device_keys] device_keys={:s}'.format(str(device_keys))) + LOGGER.debug('[compute_device_keys] end') + return device_keys + +def compute_endpoint_keys( + device_keys : Set[str], endpoint_uuid_or_name : str, endpoint_name_mapping : Dict[str, str] +) -> Set[str]: + LOGGER.debug('[compute_endpoint_keys] begin') + LOGGER.debug('[compute_endpoint_keys] device_keys={:s}'.format(str(device_keys))) + LOGGER.debug('[compute_endpoint_keys] endpoint_uuid_or_name={:s}'.format(str(endpoint_uuid_or_name))) + #LOGGER.debug('[compute_device_endpoint_keys] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + endpoint_keys = {endpoint_uuid_or_name} + for k,v in endpoint_name_mapping.items(): + if (k[0] in device_keys or v in device_keys) and (endpoint_uuid_or_name in {k[1], v}): + endpoint_keys.add(k[1]) + endpoint_keys.add(v) + + LOGGER.debug('[compute_endpoint_keys] endpoint_keys={:s}'.format(str(endpoint_keys))) + LOGGER.debug('[compute_endpoint_keys] end') + return endpoint_keys + +def compute_device_endpoint_keys( + device_uuid_or_name : str, endpoint_uuid_or_name : str, + device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] +) -> Set[Tuple[str, str]]: + LOGGER.debug('[compute_device_endpoint_keys] begin') + LOGGER.debug('[compute_device_endpoint_keys] device_uuid_or_name={:s}'.format(str(device_uuid_or_name))) + LOGGER.debug('[compute_device_endpoint_keys] endpoint_uuid_or_name={:s}'.format(str(endpoint_uuid_or_name))) + #LOGGER.debug('[compute_device_endpoint_keys] device_name_mapping={:s}'.format(str(device_name_mapping))) + #LOGGER.debug('[compute_device_endpoint_keys] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + device_keys = compute_device_keys(device_uuid_or_name, device_name_mapping) + endpoint_keys = compute_endpoint_keys(device_keys, endpoint_uuid_or_name, endpoint_name_mapping) + device_endpoint_keys = set(itertools.product(device_keys, endpoint_keys)) + + LOGGER.debug('[compute_device_endpoint_keys] device_endpoint_keys={:s}'.format(str(device_endpoint_keys))) + LOGGER.debug('[compute_device_endpoint_keys] end') + return device_endpoint_keys + +def generate_neighbor_endpoint_config_rules( + config_rules : List[Dict], path_hops : List[Dict], + device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] +) -> List[Dict]: + LOGGER.debug('[generate_neighbor_endpoint_config_rules] begin') + LOGGER.debug('[generate_neighbor_endpoint_config_rules] config_rules={:s}'.format(str(config_rules))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] path_hops={:s}'.format(str(path_hops))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] device_name_mapping={:s}'.format(str(device_name_mapping))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + generated_config_rules = list() + for link_endpoint_a, link_endpoint_b in pairwise(path_hops): + LOGGER.debug('[generate_neighbor_endpoint_config_rules] loop begin') + LOGGER.debug('[generate_neighbor_endpoint_config_rules] link_endpoint_a={:s}'.format(str(link_endpoint_a))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] link_endpoint_b={:s}'.format(str(link_endpoint_b))) + + device_endpoint_keys_a = compute_device_endpoint_keys( + link_endpoint_a['device'], link_endpoint_a['egress_ep'], + device_name_mapping, endpoint_name_mapping + ) + + device_endpoint_keys_b = compute_device_endpoint_keys( + link_endpoint_b['device'], link_endpoint_b['ingress_ep'], + device_name_mapping, endpoint_name_mapping + ) + + for config_rule in config_rules: + # Only applicable, by now, to Custom Config Rules for endpoint settings + if 'custom' not in config_rule: continue + match = RE_ENDPOINT_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: continue + + resource_key_values = match.groups() + if resource_key_values[0:2] in device_endpoint_keys_a: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_b['device'] + resource_key_values[1] = link_endpoint_b['ingress_ep'] + elif resource_key_values[0:2] in device_endpoint_keys_b: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_a['device'] + resource_key_values[1] = link_endpoint_a['egress_ep'] + else: + continue + + device_keys = compute_device_keys(resource_key_values[0], device_name_mapping) + device_names = {device_key for device_key in device_keys if RE_UUID.match(device_key) is None} + if len(device_names) != 1: + MSG = 'Unable to identify name for Device({:s}): device_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[0]), str(device_keys))) + resource_key_values[0] = device_names.pop() + + endpoint_keys = compute_endpoint_keys(device_keys, resource_key_values[1], endpoint_name_mapping) + endpoint_names = {endpoint_key for endpoint_key in endpoint_keys if RE_UUID.match(endpoint_key) is None} + if len(endpoint_names) != 1: + MSG = 'Unable to identify name for Endpoint({:s}): endpoint_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[1]), str(endpoint_keys))) + resource_key_values[1] = endpoint_names.pop() + + resource_value : Dict = json.loads(config_rule['custom']['resource_value']) + if 'neighbor_address' not in resource_value: continue + resource_value['ip_address'] = resource_value.pop('neighbor_address') + + # remove neighbor_address also from original rule as it is already consumed + + resource_key_template = TMPL_ENDPOINT_VLAN_SETTINGS if len(match.groups()) == 3 else TMPL_ENDPOINT_SETTINGS + generated_config_rule = copy.deepcopy(config_rule) + generated_config_rule['custom']['resource_key'] = resource_key_template.format(*resource_key_values) + generated_config_rule['custom']['resource_value'] = json.dumps(resource_value) + generated_config_rules.append(generated_config_rule) + + LOGGER.debug('[generate_neighbor_endpoint_config_rules] generated_config_rules={:s}'.format(str(generated_config_rules))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] end') + return generated_config_rules -- GitLab From 27ec6035456b32f4f01d482a12e6207f99c687ac Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 6 Feb 2024 14:15:02 +0000 Subject: [PATCH 15/19] NBI component: - Updated service endpoint settings custom resource key to /device/endpoint/settings - Corrected test files --- .../service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py | 6 ++++-- src/nbi/tests/data/ietf_l3vpn_req_svc2.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py index 3466c8598..80c7b32dd 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py @@ -91,8 +91,10 @@ def update_service_endpoint( for (ip_range, ip_prefix_len, lan_tag), next_hop in static_routing.items() }) - ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' - endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, vlan_tag) + #ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' + #endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, vlan_tag) + ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/settings' + endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid) field_updates = {} if vlan_tag is not None: field_updates['vlan_tag' ] = (vlan_tag, True) if ipv4_address is not None: field_updates['ip_address' ] = (ipv4_address, True) diff --git a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json index 4ecf3c2ea..2cc512e59 100644 --- a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json +++ b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json @@ -80,7 +80,7 @@ "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { - "lan": "172.1.101.1/24", + "lan": "172.1.201.1/24", "lan-tag": "vlan31", "next-hop": "128.32.33.254" } @@ -145,7 +145,7 @@ "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { - "lan": "172.1.101.1/24", + "lan": "172.1.201.1/24", "lan-tag": "vlan201", "next-hop": "172.10.33.2" } -- GitLab From c51a694db38736aaa8787cea92377b287d4a0a45 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 6 Feb 2024 14:27:42 +0000 Subject: [PATCH 16/19] Service component - L3VPN - IETF ACTN Service Handler: - Implemented rule composition - Added missing python requirement --- src/service/requirements.in | 1 + .../l3nm_ietf_actn/ConfigRuleComposer.py | 128 --------- .../l3nm_ietf_actn/Constants.py | 52 ++++ .../L3NMIetfActnServiceHandler.py | 267 ++++++++++++++---- 4 files changed, 265 insertions(+), 183 deletions(-) delete mode 100644 src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py create mode 100644 src/service/service/service_handlers/l3nm_ietf_actn/Constants.py diff --git a/src/service/requirements.in b/src/service/requirements.in index 48fd76485..a10f7da7a 100644 --- a/src/service/requirements.in +++ b/src/service/requirements.in @@ -15,6 +15,7 @@ anytree==2.8.0 geopy==2.3.0 +netaddr==0.9.0 networkx==2.6.3 pydot==1.4.2 redis==4.1.2 diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py deleted file mode 100644 index deb096b06..000000000 --- a/src/service/service/service_handlers/l3nm_ietf_actn/ConfigRuleComposer.py +++ /dev/null @@ -1,128 +0,0 @@ -# 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 typing import Dict, List, Optional, Tuple -from common.proto.context_pb2 import Device, EndPoint -from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set - -from service.service.service_handler_api.AnyTreeTools import TreeNode - -def _interface( - if_name : str, ipv4_address : str, ipv4_prefix_length : int, enabled : bool, - vlan_id : Optional[int] = None, sif_index : Optional[int] = 1 -) -> Tuple[str, Dict]: - str_path = '/interface[{:s}]'.format(if_name) - data = { - 'name': if_name, 'enabled': enabled, 'sub_if_index': sif_index, - 'sub_if_enabled': enabled, 'sub_if_ipv4_enabled': enabled, - 'sub_if_ipv4_address': ipv4_address, 'sub_if_ipv4_prefix_length': ipv4_prefix_length - } - if vlan_id is not None: data['sub_if_vlan'] = vlan_id - return str_path, data - -def _network_instance(ni_name, ni_type) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]'.format(ni_name) - data = {'name': ni_name, 'type': ni_type} - return str_path, data - -def _network_instance_static_route(ni_name, prefix, next_hop, next_hop_index=0) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]/static_route[{:s}]'.format(ni_name, prefix) - data = {'name': ni_name, 'prefix': prefix, 'next_hop': next_hop, 'next_hop_index': next_hop_index} - return str_path, data - -def _network_instance_interface(ni_name, if_name, sif_index) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]/interface[{:s}.{:d}]'.format(ni_name, if_name, sif_index) - data = {'name': ni_name, 'if_name': if_name, 'sif_index': sif_index} - return str_path, data - -class EndpointComposer: - def __init__(self, endpoint_uuid : str) -> None: - self.uuid = endpoint_uuid - self.objekt : Optional[EndPoint] = None - self.sub_interface_index = 0 - self.ipv4_address = None - self.ipv4_prefix_length = None - self.sub_interface_vlan_id = 0 - - def configure(self, endpoint_obj : EndPoint, settings : Optional[TreeNode]) -> None: - self.objekt = endpoint_obj - if settings is None: return - json_settings : Dict = settings.value - self.ipv4_address = json_settings['ipv4_address'] - self.ipv4_prefix_length = json_settings['ipv4_prefix_length'] - self.sub_interface_index = json_settings['sub_interface_index'] - self.sub_interface_vlan_id = json_settings['sub_interface_vlan_id'] - - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: - json_config_rule = json_config_rule_delete if delete else json_config_rule_set - return [ - json_config_rule(*_interface( - self.objekt.name, self.ipv4_address, self.ipv4_prefix_length, True, - sif_index=self.sub_interface_index, vlan_id=self.sub_interface_vlan_id, - )), - json_config_rule(*_network_instance_interface( - network_instance_name, self.objekt.name, self.sub_interface_index - )), - ] - -class DeviceComposer: - def __init__(self, device_uuid : str) -> None: - self.uuid = device_uuid - self.objekt : Optional[Device] = None - self.endpoints : Dict[str, EndpointComposer] = dict() - self.static_routes : Dict[str, str] = dict() - - def get_endpoint(self, endpoint_uuid : str) -> EndpointComposer: - if endpoint_uuid not in self.endpoints: - self.endpoints[endpoint_uuid] = EndpointComposer(endpoint_uuid) - return self.endpoints[endpoint_uuid] - - def configure(self, device_obj : Device, settings : Optional[TreeNode]) -> None: - self.objekt = device_obj - if settings is None: return - json_settings : Dict = settings.value - static_routes = json_settings.get('static_routes', []) - for static_route in static_routes: - prefix = static_route['prefix'] - next_hop = static_route['next_hop'] - self.static_routes[prefix] = next_hop - - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: - json_config_rule = json_config_rule_delete if delete else json_config_rule_set - config_rules = [ - json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) - ] - for endpoint in self.endpoints.values(): - config_rules.extend(endpoint.get_config_rules(network_instance_name, delete=delete)) - for prefix, next_hop in self.static_routes.items(): - config_rules.append( - json_config_rule(*_network_instance_static_route(network_instance_name, prefix, next_hop)) - ) - if delete: config_rules = list(reversed(config_rules)) - return config_rules - -class ConfigRuleComposer: - def __init__(self) -> None: - self.devices : Dict[str, DeviceComposer] = dict() - - def get_device(self, device_uuid : str) -> DeviceComposer: - if device_uuid not in self.devices: - self.devices[device_uuid] = DeviceComposer(device_uuid) - return self.devices[device_uuid] - - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> Dict[str, List[Dict]]: - return { - device_uuid : device.get_config_rules(network_instance_name, delete=delete) - for device_uuid, device in self.devices.items() - } diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py b/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py new file mode 100644 index 000000000..62babd7c2 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py @@ -0,0 +1,52 @@ +# 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. + +# These hardcoded values will be updated with proper logic in second phase of the PoC + +VPN_VLAN_TAGS_TO_SERVICE_NAME = { + (21, 101): ('osu_tunnel_1', 'etht_service_1'), + (31, 201): ('osu_tunnel_2', 'etht_service_2'), +} + +OSU_TUNNEL_SETTINGS = { + 'osu_tunnel_1': { + 'odu_type': 'osuflex', + 'osuflex_number': 40, + 'bidirectional': True, + 'delay': 20, + 'ttp_channel_names': { + ('10.0.10.1', '200'): 'och:1-odu2:1-oduflex:1-osuflex:2', + ('10.0.30.1', '200'): 'och:1-odu2:1-oduflex:3-osuflex:1', + } + }, + 'osu_tunnel_2': { + 'odu_type': 'osuflex', + 'osuflex_number': 40, + 'bidirectional': True, + 'delay': 20, + 'ttp_channel_names': { + ('10.0.10.1', '200'): 'och:1-odu2:1-oduflex:1-osuflex:2', + ('10.0.30.1', '200'): 'och:1-odu2:1-oduflex:3-osuflex:1', + } + }, +} + +ETHT_SERVICE_SETTINGS = { + 'etht_service_1': { + 'service_type': 'op-mp2mp-svc', + }, + 'etht_service_2': { + 'service_type': 'op-mp2mp-svc', + }, +} diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py index 4b53ac0d2..0c20fdf96 100644 --- a/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py +++ b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py @@ -12,17 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, logging +import json, logging, netaddr from typing import Any, Dict, List, Optional, Tuple, Union from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method -from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.proto.context_pb2 import ConfigRule, Device, DeviceId, EndPoint, Service +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set from common.tools.object_factory.Device import json_device_id from common.type_checkers.Checkers import chk_type from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching from service.service.service_handler_api._ServiceHandler import _ServiceHandler from service.service.service_handler_api.SettingsHandler import SettingsHandler from service.service.task_scheduler.TaskExecutor import TaskExecutor -from .ConfigRuleComposer import ConfigRuleComposer +from .Constants import ETHT_SERVICE_SETTINGS, OSU_TUNNEL_SETTINGS, VPN_VLAN_TAGS_TO_SERVICE_NAME LOGGER = logging.getLogger(__name__) @@ -35,79 +37,234 @@ class L3NMIetfActnServiceHandler(_ServiceHandler): self.__service = service self.__task_executor = task_executor self.__settings_handler = SettingsHandler(service.service_config, **settings) - self.__composer = ConfigRuleComposer() - self.__endpoint_map : Dict[Tuple[str, str], str] = dict() - def _compose_config_rules(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> None: - for endpoint in endpoints: - device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + def _get_endpoint_details( + self, endpoint : Tuple[str, str, Optional[str]] + ) -> Tuple[Device, EndPoint, Dict]: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + device_name = device_obj.name + endpoint_name = endpoint_obj.name + if endpoint_settings is None: + MSG = 'Settings not found for Endpoint(device=[uuid={:s}, name={:s}], endpoint=[uuid={:s}, name={:s}])' + raise Exception(MSG.format(device_uuid, device_name, endpoint_uuid, endpoint_name)) + endpoint_settings_dict : Dict = endpoint_settings.value + return device_obj, endpoint_obj, endpoint_settings_dict - device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) - device_settings = self.__settings_handler.get_device_settings(device_obj) - _device = self.__composer.get_device(device_obj.name) - _device.configure(device_obj, device_settings) + def _get_service_names( + self, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict] + ) -> Tuple[str, str]: + _, _, src_endpoint_settings_dict = src_endpoint_details + src_vlan_tag = src_endpoint_settings_dict['vlan_tag'] - endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) - endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) - _endpoint = _device.get_endpoint(endpoint_obj.name) - _endpoint.configure(endpoint_obj, endpoint_settings) + _, _, dst_endpoint_settings_dict = dst_endpoint_details + dst_vlan_tag = dst_endpoint_settings_dict['vlan_tag'] - self.__endpoint_map[(device_uuid, endpoint_uuid)] = device_obj.name + service_names = VPN_VLAN_TAGS_TO_SERVICE_NAME.get((src_vlan_tag, dst_vlan_tag)) + if service_names is None: + MSG = 'Unable to find service names from VLAN tags(src={:s}, dst={:s})' + raise Exception(MSG.format(str(src_vlan_tag), str(dst_vlan_tag))) + return service_names - def _do_configurations( - self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], - delete : bool = False - ) -> List[Union[bool, Exception]]: - # Configuration is done atomically on each device, all OK / all KO per device - results_per_device = dict() - for device_name,json_config_rules in config_rules_per_device.items(): - try: - device_obj = self.__composer.get_device(device_name).objekt - if len(json_config_rules) == 0: continue - del device_obj.device_config.config_rules[:] - for json_config_rule in json_config_rules: - device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) - self.__task_executor.configure_device(device_obj) - results_per_device[device_name] = True - except Exception as e: # pylint: disable=broad-exception-caught - verb = 'deconfigure' if delete else 'configure' - MSG = 'Unable to {:s} Device({:s}) : ConfigRules({:s})' - LOGGER.exception(MSG.format(verb, str(device_name), str(json_config_rules))) - results_per_device[device_name] = e + def _compose_osu_tunnel( + self, osu_tunnel_name : str, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict], + is_delete : bool = False + ) -> ConfigRule: + osu_tunnel_resource_key = '/osu_tunnels/osu_tunnel[{:s}]'.format(osu_tunnel_name) + osu_tunnel_resource_value = {'name' : osu_tunnel_name} + if is_delete: + osu_tunnel_config_rule = json_config_rule_delete(osu_tunnel_resource_key, osu_tunnel_resource_value) + else: + src_device_obj, src_endpoint_obj, _ = src_endpoint_details + dst_device_obj, dst_endpoint_obj, _ = dst_endpoint_details - results = [] - for endpoint in endpoints: - device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) - device_name = self.__endpoint_map[(device_uuid, endpoint_uuid)] - results.append(results_per_device[device_name]) - return results + osu_tunnel_settings = OSU_TUNNEL_SETTINGS[osu_tunnel_name] + ttp_channel_names = osu_tunnel_settings['ttp_channel_names'] + src_ttp_channel_name = ttp_channel_names[(src_device_obj.name, src_endpoint_obj.name)] + dst_ttp_channel_name = ttp_channel_names[(dst_device_obj.name, dst_endpoint_obj.name)] + + osu_tunnel_resource_value.update({ + 'odu_type' : osu_tunnel_settings['odu_type'], + 'osuflex_number' : osu_tunnel_settings['osuflex_number'], + 'bidirectional' : osu_tunnel_settings['bidirectional'], + 'delay' : osu_tunnel_settings['delay'], + 'src_node_id' : src_device_obj.name, + 'src_tp_id' : src_endpoint_obj.name, + 'src_ttp_channel_name': src_ttp_channel_name, + 'dst_node_id' : dst_device_obj.name, + 'dst_tp_id' : dst_endpoint_obj.name, + 'dst_ttp_channel_name': dst_ttp_channel_name, + }) + osu_tunnel_config_rule = json_config_rule_set(osu_tunnel_resource_key, osu_tunnel_resource_value) + LOGGER.debug('osu_tunnel_config_rule = {:s}'.format(str(osu_tunnel_config_rule))) + return ConfigRule(**osu_tunnel_config_rule) + + def _compose_static_routing( + self, src_vlan_tag : int, dst_vlan_tag : int + ) -> Tuple[List[Dict], List[Dict]]: + static_routing = self.__settings_handler.get('/static_routing') + if static_routing is None: raise Exception('static_routing not found') + static_routing_dict : Dict = static_routing.value + src_static_routes = list() + dst_static_routes = list() + for _, static_route in static_routing_dict.items(): + vlan_id = static_route['vlan-id'] + ipn_cidr = netaddr.IPNetwork(static_route['ip-network']) + ipn_network = str(ipn_cidr.network) + ipn_preflen = int(ipn_cidr.prefixlen) + next_hop = static_route['next-hop'] + if vlan_id == src_vlan_tag: + src_static_routes.append([ipn_network, ipn_preflen, next_hop]) + elif vlan_id == dst_vlan_tag: + dst_static_routes.append([ipn_network, ipn_preflen, next_hop]) + return src_static_routes, dst_static_routes + + def _compose_etht_service( + self, etht_service_name : str, osu_tunnel_name : str, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict], + is_delete : bool = False + ) -> ConfigRule: + etht_service_resource_key = '/etht_services/etht_service[{:s}]'.format(etht_service_name) + etht_service_resource_value = {'name' : etht_service_name} + if is_delete: + etht_service_config_rule = json_config_rule_delete(etht_service_resource_key, etht_service_resource_value) + else: + src_device_obj, src_endpoint_obj, src_endpoint_details = src_endpoint_details + src_vlan_tag = src_endpoint_details['vlan_tag'] + dst_device_obj, dst_endpoint_obj, dst_endpoint_details = dst_endpoint_details + dst_vlan_tag = dst_endpoint_details['vlan_tag'] + src_static_routes, dst_static_routes = self._compose_static_routing(src_vlan_tag, dst_vlan_tag) + etht_service_resource_value.update({ + 'osu_tunnel_name' : osu_tunnel_name, + 'service_type' : ETHT_SERVICE_SETTINGS[etht_service_name]['service_type'], + 'src_node_id' : src_device_obj.name, + 'src_tp_id' : src_endpoint_obj.name, + 'src_vlan_tag' : src_vlan_tag, + 'src_static_routes': src_static_routes, + 'dst_node_id' : dst_device_obj.name, + 'dst_tp_id' : dst_endpoint_obj.name, + 'dst_vlan_tag' : dst_vlan_tag, + 'dst_static_routes': dst_static_routes, + }) + etht_service_config_rule = json_config_rule_set(etht_service_resource_key, etht_service_resource_value) + LOGGER.debug('etht_service_config_rule = {:s}'.format(str(etht_service_config_rule))) + return ConfigRule(**etht_service_config_rule) @metered_subclass_method(METRICS_POOL) def SetEndpoint( self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None ) -> List[Union[bool, Exception]]: + LOGGER.debug('endpoints = {:s}'.format(str(endpoints))) chk_type('endpoints', endpoints, list) - if len(endpoints) == 0: return [] + if len(endpoints) < 2: + LOGGER.warning('nothing done: not enough endpoints') + return [] service_uuid = self.__service.service_id.service_uuid.uuid - #settings = self.__settings_handler.get('/settings') - self._compose_config_rules(endpoints) - network_instance_name = service_uuid.split('-')[0] - config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=False) - results = self._do_configurations(config_rules_per_device, endpoints) + LOGGER.debug('service_uuid = {:s}'.format(str(service_uuid))) + LOGGER.debug('self.__settings_handler = {:s}'.format(str(self.__settings_handler.dump_config_rules()))) + + results = [] + try: + src_endpoint_details = self._get_endpoint_details(endpoints[0]) + src_device_obj, _, _ = src_endpoint_details + src_controller = self.__task_executor.get_device_controller(src_device_obj) + if src_controller is None: src_controller = src_device_obj + + dst_endpoint_details = self._get_endpoint_details(endpoints[-1]) + dst_device_obj, _, _ = dst_endpoint_details + dst_controller = self.__task_executor.get_device_controller(dst_device_obj) + if dst_controller is None: dst_controller = dst_device_obj + + if src_controller.device_id.device_uuid.uuid != dst_controller.device_id.device_uuid.uuid: + raise Exception('Different Src-Dst devices not supported by now') + controller = src_controller + + osu_tunnel_name, etht_service_name = self._get_service_names( + src_endpoint_details, dst_endpoint_details + ) + + osu_tunnel_config_rule = self._compose_osu_tunnel( + osu_tunnel_name, src_endpoint_details, dst_endpoint_details, + is_delete=False + ) + + etht_service_config_rule = self._compose_etht_service( + etht_service_name, osu_tunnel_name, src_endpoint_details, + dst_endpoint_details, is_delete=False + ) + + del controller.device_config.config_rules[:] + controller.device_config.config_rules.append(osu_tunnel_config_rule) + controller.device_config.config_rules.append(etht_service_config_rule) + self.__task_executor.configure_device(controller) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetEndpoint for Service({:s})'.format(str(service_uuid))) + results.append(e) + + LOGGER.debug('results = {:s}'.format(str(results))) return results @metered_subclass_method(METRICS_POOL) def DeleteEndpoint( self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None ) -> List[Union[bool, Exception]]: + LOGGER.debug('endpoints = {:s}'.format(str(endpoints))) chk_type('endpoints', endpoints, list) - if len(endpoints) == 0: return [] + if len(endpoints) < 2: + LOGGER.warning('nothing done: not enough endpoints') + return [] service_uuid = self.__service.service_id.service_uuid.uuid - #settings = self.__settings_handler.get('/settings') - self._compose_config_rules(endpoints) - network_instance_name = service_uuid.split('-')[0] - config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=True) - results = self._do_configurations(config_rules_per_device, endpoints, delete=True) + LOGGER.debug('service_uuid = {:s}'.format(str(service_uuid))) + LOGGER.debug('self.__settings_handler = {:s}'.format(str(self.__settings_handler.dump_config_rules()))) + + results = [] + try: + src_endpoint_details = self._get_endpoint_details(endpoints[0]) + src_device_obj, _, _ = src_endpoint_details + src_controller = self.__task_executor.get_device_controller(src_device_obj) + if src_controller is None: src_controller = src_device_obj + + dst_endpoint_details = self._get_endpoint_details(endpoints[-1]) + dst_device_obj, _, _ = dst_endpoint_details + dst_controller = self.__task_executor.get_device_controller(dst_device_obj) + if dst_controller is None: dst_controller = dst_device_obj + + if src_controller.device_id.device_uuid.uuid != dst_controller.device_id.device_uuid.uuid: + raise Exception('Different Src-Dst devices not supported by now') + controller = src_controller + + osu_tunnel_name, etht_service_name = self._get_service_names( + src_endpoint_details, dst_endpoint_details + ) + + osu_tunnel_config_rule = self._compose_osu_tunnel( + osu_tunnel_name, src_endpoint_details, dst_endpoint_details, + is_delete=True + ) + + etht_service_config_rule = self._compose_etht_service( + etht_service_name, osu_tunnel_name, src_endpoint_details, + dst_endpoint_details, is_delete=True + ) + + del controller.device_config.config_rules[:] + controller.device_config.config_rules.append(osu_tunnel_config_rule) + controller.device_config.config_rules.append(etht_service_config_rule) + self.__task_executor.configure_device(controller) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteEndpoint for Service({:s})'.format(str(service_uuid))) + results.append(e) + + LOGGER.debug('results = {:s}'.format(str(results))) return results @metered_subclass_method(METRICS_POOL) -- GitLab From 1c1a378565508d131f2769015c6ffd656bc8f121 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 6 Feb 2024 16:33:24 +0000 Subject: [PATCH 17/19] NBI component - IETF Network: - Added missing device type filters --- .../rest_server/nbi_plugins/ietf_network/ComposeNetwork.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py index 6ffc85e38..2d3ef29fc 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py @@ -28,9 +28,11 @@ IGNORE_DEVICE_TYPES = { DeviceTypeEnum.DATACENTER.value, DeviceTypeEnum.EMULATED_CLIENT.value, DeviceTypeEnum.EMULATED_DATACENTER.value, + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value, DeviceTypeEnum.EMULATED_XR_CONSTELLATION.value, + DeviceTypeEnum.IP_SDN_CONTROLLER, DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value, DeviceTypeEnum.NETWORK.value, DeviceTypeEnum.OPEN_LINE_SYSTEM.value, @@ -39,10 +41,10 @@ IGNORE_DEVICE_TYPES = { IGNORE_DEVICE_NAMES = { NetworkTypeEnum.TE_OTN_TOPOLOGY: { - '128.32.10.1', '128.32.33.5', '128.32.20.5', '128.32.20.1', '128.32.10.5', 'nce-t' + 'nce-t', '128.32.10.1', '128.32.33.5', '128.32.20.5', '128.32.20.1', '128.32.10.5', }, NetworkTypeEnum.TE_ETH_TRAN_TOPOLOGY: { - + 'nce-t', }, } -- GitLab From 0b09e57583d55cf5e2540b59d84d853b0c463302 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 6 Feb 2024 17:16:40 +0000 Subject: [PATCH 18/19] Pre-merge clean-up --- manifests/deviceservice.yaml | 2 +- manifests/nbiservice.yaml | 2 +- manifests/pathcompservice.yaml | 4 ++-- manifests/serviceservice.yaml | 2 +- manifests/webuiservice.yaml | 2 +- src/common/tools/descriptor/Loader.py | 20 +++++++++---------- .../algorithms/tools/ComposeConfigRules.py | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/manifests/deviceservice.yaml b/manifests/deviceservice.yaml index 7f7885daf..77e421f29 100644 --- a/manifests/deviceservice.yaml +++ b/manifests/deviceservice.yaml @@ -39,7 +39,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" startupProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:2020"] diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml index f5477aeb4..de97ba364 100644 --- a/manifests/nbiservice.yaml +++ b/manifests/nbiservice.yaml @@ -37,7 +37,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:9090"] diff --git a/manifests/pathcompservice.yaml b/manifests/pathcompservice.yaml index 0ebd1811b..87d907a72 100644 --- a/manifests/pathcompservice.yaml +++ b/manifests/pathcompservice.yaml @@ -36,9 +36,9 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: ENABLE_FORECASTER - value: "NO" + value: "YES" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:10020"] diff --git a/manifests/serviceservice.yaml b/manifests/serviceservice.yaml index 3865fd6c0..7d7bdaa4e 100644 --- a/manifests/serviceservice.yaml +++ b/manifests/serviceservice.yaml @@ -36,7 +36,7 @@ spec: - containerPort: 9192 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" readinessProbe: exec: command: ["/bin/grpc_health_probe", "-addr=:3030"] diff --git a/manifests/webuiservice.yaml b/manifests/webuiservice.yaml index 89de36fc5..43caa9f04 100644 --- a/manifests/webuiservice.yaml +++ b/manifests/webuiservice.yaml @@ -39,7 +39,7 @@ spec: - containerPort: 8004 env: - name: LOG_LEVEL - value: "DEBUG" + value: "INFO" - name: WEBUISERVICE_SERVICE_BASEURL_HTTP value: "/webui/" readinessProbe: diff --git a/src/common/tools/descriptor/Loader.py b/src/common/tools/descriptor/Loader.py index 11500a32b..4ab33beae 100644 --- a/src/common/tools/descriptor/Loader.py +++ b/src/common/tools/descriptor/Loader.py @@ -57,20 +57,20 @@ LOGGERS = { ENTITY_TO_TEXT = { # name => singular, plural - 'context' : ('Context', 'Contexts' ), - 'topology' : ('Topology', 'Topologies' ), - 'controller': ('Controller', 'Controllers' ), - 'device' : ('Device', 'Devices' ), - 'link' : ('Link', 'Links' ), - 'service' : ('Service', 'Services' ), - 'slice' : ('Slice', 'Slices' ), - 'connection': ('Connection', 'Connections' ), + 'context' : ('Context', 'Contexts' ), + 'topology' : ('Topology', 'Topologies' ), + 'controller': ('Controller', 'Controllers'), + 'device' : ('Device', 'Devices' ), + 'link' : ('Link', 'Links' ), + 'service' : ('Service', 'Services' ), + 'slice' : ('Slice', 'Slices' ), + 'connection': ('Connection', 'Connections'), } ACTION_TO_TEXT = { # action => infinitive, past - 'add' : ('Add', 'Added'), - 'update' : ('Update', 'Updated'), + 'add' : ('Add', 'Added' ), + 'update' : ('Update', 'Updated' ), 'config' : ('Configure', 'Configured'), } diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 8e99a1ae1..2d4ff4fd5 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -159,7 +159,7 @@ def compose_device_config_rules( match = RE_DEVICE_SETTINGS.match(config_rule.custom.resource_key) if match is not None: device_uuid_or_name = match.group(1) - device_keys = {'?', device_uuid_or_name} + device_keys = {device_uuid_or_name} device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) @@ -171,12 +171,12 @@ def compose_device_config_rules( match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule.custom.resource_key) if match is not None: device_uuid_or_name = match.group(1) - device_keys = {'?', device_uuid_or_name} + device_keys = {device_uuid_or_name} device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) endpoint_uuid_or_name = match.group(2) - endpoint_keys = {'?', endpoint_uuid_or_name} + endpoint_keys = {endpoint_uuid_or_name} endpoint_name_or_uuid_1 = endpoint_name_mapping.get((device_uuid_or_name, endpoint_uuid_or_name)) if endpoint_name_or_uuid_1 is not None: endpoint_keys.add(endpoint_name_or_uuid_1) endpoint_name_or_uuid_2 = endpoint_name_mapping.get((device_name_or_uuid, endpoint_uuid_or_name)) -- GitLab From a7944f9eb95fb1838720692e654619cfdf140f0c Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Tue, 6 Feb 2024 17:17:26 +0000 Subject: [PATCH 19/19] Tests > F5G PoC CAMARA: - Added test instructions and example descriptor. --- src/tests/f5g-poc-camara/POC-CAMARA-Guide.md | 162 +++++++++++ .../f5g-poc-camara/data/topology-real.json | 260 ++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 src/tests/f5g-poc-camara/POC-CAMARA-Guide.md create mode 100644 src/tests/f5g-poc-camara/data/topology-real.json diff --git a/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md b/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md new file mode 100644 index 000000000..85ec44cb6 --- /dev/null +++ b/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md @@ -0,0 +1,162 @@ +# TeraFlowSDN - ETSI F5G PoC CAMARA Guide + +This guide describes how to: +1. Configure and Deploy TeraFlowSDN for the ETSI F5G PoC CAMARA +2. Start Mock IETF ACTN SDN Controller (for testing and debugging) +3. Onboard the network topology descriptor +4. Expose the topology through the RESTConf IETF Network endpoint +5. Create Services through RESTConf IETF L3VPN endpoint +6. Get State of Services through RESTConf IETF L3VPN endpoint +7. Check configurations done in the Mock IETF ACTN SDN Controller (for testing and debugging) +8. Destroy Services through RESTConf IETF L3VPN endpoint + + +## 1. Configure and Deploy TeraFlowSDN for the ETSI F5G PoC CAMARA + +This guide assumes the user pre-configured a physical/virtual machine based on the steps described in +the official +[ETSI TeraFlowSDN - Deployment Guide](https://labs.etsi.org/rep/tfs/controller/-/wikis/1.-Deployment-Guide). + +__NOTE__: When you perform step _1.3. Deploy TeraFlowSDN_, configure the `my_deploy.sh` script modifying +the following settings: +```bash +# ... +export TFS_COMPONENTS="context device pathcomp service slice nbi webui" +# ... +export CRDB_DROP_DATABASE_IF_EXISTS="YES" +# ... +export QDB_DROP_TABLES_IF_EXIST="YES" +# ... +``` + +After modifying the file, deploy the TeraFlowSDN using the regular script `./deploy/all.sh`. +The script might take a while to run, especially the first time, since it needs to build the TeraFlowSDN +microservices. + + +## 2. Start Mock IETF ACTN SDN Controller (for testing and debugging) + +__NOTE__: This step is not needed when using the real NCE-T controller. + +Start the Mock IETF ACTN SDN Controller. This controller is a simple Python script that accepts requests +based on agreed F5G PoC CAMARA and stores it in memory, mimicking the NCE-T controller. + +Run the Mock IETF ACTN SDN Controller as follows: +```bash +python src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +``` + + +## 3. Onboard the network topology descriptor + +The network topology descriptor is a TeraFlowSDN configuration file describing the different elements to be +managed by the SDN controller, such as devices, links, networks, etc. A preliminary descriptor has been +prepared for the PoC CAMARA. The file is named as `topology-real.json`. + +**NOTE**: Before onboarding file `topology-real.json`, update settings of device `nce-t` to match the IP +address, port, username, password, HTTP scheme, etc. of the real NCE-T. + +To onboard the descriptor file, navigate to the [TeraFlowSDN WebUI](http://127.0.0.1/webui) > Home. +Browse the file through the _Descriptors_ field, and click the _Submit_ button that is next to the field. +The onboarding should take few seconds and the WebUI should report that 1 context, 1 topology, 1 controller, +10 devices, and 24 links were added. Also, it should report that 1 topology was updated. + +Next, select in the field _Ctx/Topo_ the entry named as `Context(admin):Topology(admin)`, and click the +_Submit_ button that is next to the field. The topology should be displayed just below. + +Then, navigate to the WebUI > Devices and WebUI > Links sections to familiarize with the details provided +for each entity. You can check the details of each entity by clicking the eye-shaped icon on the right +side of each row. + +The underlying devices are configured as EMULATED entities while the NCE-T controller is configured as an +IP SDN controller. Auto-discovery of devices is not implemented as this will fall in PoC phase two. + + +## 4. Expose the topology through the RESTConf IETF Network endpoint + +The TeraFlowSDN controller features an NBI component that exposes RESTConf-based endpoints. To retrieve the +topology following the IETF Network data model, use the following `curl` (or similar) command: + +```bash +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-network:networks/ +``` + +__NOTE__: The command requires to interrogate the complete database and might take few seconds to complete. + + +## 5. Create Services through RESTConf IETF L3VPN endpoint + +The TeraFlowSDN controller's NBI component also exposes the IETF L3VPN endpoints to +create/check_status/delete services. To try them, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin -X POST -H "Content-Type: application/json" -d @src/nbi/tests/data/ietf_l3vpn_req_svc1.json http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services +curl -u admin:admin -X POST -H "Content-Type: application/json" -d @src/nbi/tests/data/ietf_l3vpn_req_svc2.json http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services +``` + +__NOTE 1__: This command uses the provided descriptors for creating the VPN services with some adaptations +to adjust to the official data model. + +__NOTE 2__: This command retrieves no data if everything succeeds, in case of error, it will be reported. + +This step will create the services in TeraFlowSDN and create the appropriate configuration rules in the +NCE-T controller through the appropriate service handlers and SBI drivers. + +When the services are created, navigate to the WebUI > Services section to familiarize with the details +provided for each service. You can check the details of the service by clicking the eye-shaped icon on +the right side of each row. + +Note that two services are created per requested VPN. The reason for that is because those services named +as "vpnX" (the name provided in the request) correspond to end-to-end services, while the others with a UUID +as a name are generated by TeraFlowSDN to represent the transport segment managed through the NCE-T. +TeraFlowSDN gathers the settings from the upper-layer end-to-end service and contructs the NCE-T-bound +services. + +Also, you can navigate to the WebUI > Devices section, click on the eye-shaped icon next to the `nce-t` +device and check the configuration rules (defined using an internal data model, not IETF ACTN) that are +later converted into the IETF ACTN configuration instructions sent to the NCE-T. + +You should see in configuration rules of the `nce-t` device rules with a resource key formatted as +`/osu_tunnels/osu_tunnel[{osu-tunnel-name}]` for each OSU tunnel, and others with resource key like +`/etht_services/etht_service[{etht-service-name}]` for each EthT service. + + +## 6. Get State of Services through RESTConf IETF L3VPN endpoint + +To get the status of the services, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn1 +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn2 +``` + +__NOTE__: This command retrieves an empty dictionary with no error if the service is ready and ACTIVE. + + +## 7. Check configurations done in the Mock IETF ACTN SDN Controller (for testing and debugging) + +__NOTE__: This step is not needed when using the real NCE-T controller. + +While running the Mock IETF ACTN SDN Controller, you can interrogate the OSU tunnels and EthT Services +created using the following commands: + +```bash +curl --insecure https://127.0.0.1:8443/restconf/v2/data/ietf-te:te/tunnels +curl --insecure https://127.0.0.1:8443/restconf/v2/data/ietf-eth-tran-service:etht-svc +``` + + +## 8. Destroy Services through RESTConf IETF L3VPN endpoint + +To destroy the services, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin -X DELETE http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn1 +curl -u admin:admin -X DELETE http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn2 +``` + +__NOTE__: This command retrieves no data when it succeeds. + +When the services are deleted, navigate to the WebUI > Services section verify that no service is present. +Besides, navigate to the WebUI > Devices section, and inspect the NCE-T device to verify that the OSU +tunnel and ETHT service configuration rules disapeared. diff --git a/src/tests/f5g-poc-camara/data/topology-real.json b/src/tests/f5g-poc-camara/data/topology-real.json new file mode 100644 index 000000000..c8c146ce2 --- /dev/null +++ b/src/tests/f5g-poc-camara/data/topology-real.json @@ -0,0 +1,260 @@ +{ + "contexts": [ + {"context_id": {"context_uuid": {"uuid": "admin"}}} + ], + + "topologies": [ + {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}} + ], + + "devices": [ + {"device_id": {"device_uuid": {"uuid": "nce-t"}}, "name": "nce-t", "device_type": "ip-sdn-controller", + "device_operational_status": 1, "device_drivers": [10], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.10"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": { + "scheme": "https", "username": "admin", "password": "admin", "base_url": "/restconf/v2/data", + "timeout": 120, "verify": false + }}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "name": "10.0.10.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "200", "name": "200", "type": "copper" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "name": "10.0.20.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "name": "10.0.30.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "200", "name": "200", "type": "copper" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "name": "10.0.40.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "name": "128.32.10.5", "device_type": "emu-client", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "name": "eth1", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "name": "128.32.10.1", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "name": "128.32.20.5", "device_type": "emu-client", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "name": "eth1", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "name": "128.32.20.1", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "name": "128.32.33.5", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "201", "name": "201", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "name": "172.10.33.5", "device_type": "emu-datacenter", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "201", "name": "201", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}} + ], + + "links": [ + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.10.1/mgmt"}}, "name": "nce-t/mgmt==10.0.10.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.20.1/mgmt"}}, "name": "nce-t/mgmt==10.0.20.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.30.1/mgmt"}}, "name": "nce-t/mgmt==10.0.30.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.40.1/mgmt"}}, "name": "nce-t/mgmt==10.0.40.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-501"}}, "name": "10.0.10.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.20.1-501"}}, "name": "10.0.20.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-500"}}, "name": "10.0.10.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.40.1-500"}}, "name": "10.0.40.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.20.1-500"}}, "name": "10.0.20.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-500"}}, "name": "10.0.30.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.40.1-501"}}, "name": "10.0.40.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-501"}}, "name": "10.0.30.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.10.5-eth1"}}, "name": "128.32.10.5-eth1", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "200" }} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.10.1-200"}}, "name": "128.32.10.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "200" }}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "endpoint_uuid": {"uuid": "eth1"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.10.1-500"}}, "name": "128.32.10.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-200"}}, "name": "128.32.33.5-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.20.5-eth1"}}, "name": "128.32.20.5-eth1", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "200" }} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.20.1-200"}}, "name": "128.32.20.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "200" }}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "endpoint_uuid": {"uuid": "eth1"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.20.1-500"}}, "name": "128.32.20.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "201"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-201"}}, "name": "128.32.33.5-201", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "201"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-500"}}, "name": "128.32.33.5-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-200"}}, "name": "10.0.10.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "172.10.33.5-500"}}, "name": "172.10.33.5-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-200"}}, "name": "10.0.30.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "endpoint_uuid": {"uuid": "500"}} + ]} + ] +} -- GitLab