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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 b-GByJjm5+HF?zt4S8-GoH5JO`;9>t4cmv%T literal 0 HcmV?d00001 -- GitLab From 514159155ef9d4c4da01a1f2a312a37a602ba358 Mon Sep 17 00:00:00 2001 From: gifrerenom 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