From 1e992e5ce84e54ce51cadf2d072eb0cab16c3fce Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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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

literal 0
HcmV?d00001

diff --git a/src/webui/service/static/topology_icons/ip-sdn-controller.png b/src/webui/service/static/topology_icons/ip-sdn-controller.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0b1abe8738b7e34faaa34d286157a778069fdbb
GIT binary patch
literal 15075
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
b-GByJjm5+HF?zt4S8-GoH5JO`;9>t4cmv%T

literal 0
HcmV?d00001

-- 
GitLab


From 514159155ef9d4c4da01a1f2a312a37a602ba358 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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 <lluis.gifre@cttc.es>
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