From 293db8511b97c30ec10aee8ff3548df7aa9e8c38 Mon Sep 17 00:00:00 2001
From: Roberto Rubino <roberto.rubino@siaemic.com>
Date: Tue, 14 Jun 2022 14:58:19 +0000
Subject: [PATCH 01/11] new dev of microwave service handler

---
 .../service/service_handlers/__init__.py      |   7 +
 .../microwave/MicrowaveServiceHandler.py      | 175 ++++++++++++++++++
 2 files changed, 182 insertions(+)
 create mode 100644 src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py

diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py
index 6abe4048f..3cff3ac09 100644
--- a/src/service/service/service_handlers/__init__.py
+++ b/src/service/service/service_handlers/__init__.py
@@ -16,6 +16,7 @@ from ..service_handler_api.FilterFields import FilterFieldEnum, ORM_DeviceDriver
 from .l3nm_emulated.L3NMEmulatedServiceHandler import L3NMEmulatedServiceHandler
 from .l3nm_openconfig.L3NMOpenConfigServiceHandler import L3NMOpenConfigServiceHandler
 from .tapi_tapi.TapiServiceHandler import TapiServiceHandler
+from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler
 
 SERVICE_HANDLERS = [
     (L3NMEmulatedServiceHandler, [
@@ -36,4 +37,10 @@ SERVICE_HANDLERS = [
             FilterFieldEnum.DEVICE_DRIVER : ORM_DeviceDriverEnum.TRANSPORT_API,
         }
     ]),
+    (MicrowaveServiceHandler, [
+        {
+            FilterFieldEnum.SERVICE_TYPE  : ORM_ServiceTypeEnum.L2NM,
+            FilterFieldEnum.DEVICE_DRIVER : ORM_DeviceDriverEnum.IETF_NETWORK_TOPOLOGY,
+        }
+    ]),
 ]
diff --git a/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
new file mode 100644
index 000000000..6ac58e303
--- /dev/null
+++ b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
@@ -0,0 +1,175 @@
+# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
+#
+# 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 anytree, json, logging
+from typing import Any, Dict, List, Optional, Tuple, Union
+from common.orm.Database import Database
+from common.orm.HighLevel import get_object
+from common.orm.backend.Tools import key_to_str
+from common.type_checkers.Checkers import chk_type
+from context.client.ContextClient import ContextClient
+from device.client.DeviceClient import DeviceClient
+from device.proto.context_pb2 import Device
+from service.service.database.ConfigModel import ORM_ConfigActionEnum, get_config_rules
+from service.service.database.ContextModel import ContextModel
+from service.service.database.DeviceModel import DeviceModel
+from service.service.database.ServiceModel import ServiceModel
+from service.service.service_handler_api._ServiceHandler import _ServiceHandler
+from service.service.service_handler_api.AnyTreeTools import TreeNode, delete_subnode, get_subnode, set_subnode_value
+from service.service.service_handlers.Tools import config_rule_set, config_rule_delete
+
+LOGGER = logging.getLogger(__name__)
+
+class MicrowaveServiceHandler(_ServiceHandler):
+    def __init__(   # pylint: disable=super-init-not-called
+        self, db_service : ServiceModel, database : Database, context_client : ContextClient,
+        device_client : DeviceClient, **settings
+    ) -> None:
+        self.__db_service = db_service
+        self.__database = database
+        self.__context_client = context_client # pylint: disable=unused-private-member
+        self.__device_client = device_client
+
+        self.__db_context : ContextModel = get_object(self.__database, ContextModel, self.__db_service.context_fk)
+        str_service_key = key_to_str([self.__db_context.context_uuid, self.__db_service.service_uuid])
+        db_config = get_config_rules(self.__database, str_service_key, 'running')
+        self.__resolver = anytree.Resolver(pathattr='name')
+        self.__config = TreeNode('.')
+        for action, resource_key, resource_value in db_config:
+            if action == ORM_ConfigActionEnum.SET:
+                try:
+                    resource_value = json.loads(resource_value)
+                except: # pylint: disable=bare-except
+                    pass
+                set_subnode_value(self.__resolver, self.__config, resource_key, resource_value)
+            elif action == ORM_ConfigActionEnum.DELETE:
+                delete_subnode(self.__resolver, self.__config, resource_key)
+
+    def SetEndpoint(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> List[Union[bool, Exception]]:
+        chk_type('endpoints', endpoints, list)
+        if len(endpoints) != 2: return []
+
+        service_uuid = self.__db_service.service_uuid
+        service_settings : TreeNode = get_subnode(self.__resolver, self.__config, 'settings', None)
+        if service_settings is None: raise Exception('Unable to settings for Service({:s})'.format(str(service_uuid)))
+
+        json_settings : Dict = service_settings.value
+        vlan_id = json_settings.get('vlan_id', 121)
+        # endpoints are retrieved in the following format --> '/endpoints/endpoint[172.26.60.243:9]'
+        try:
+            endpoint_src_split = endpoints[0][1].split(':')
+            endpoint_dst_split = endpoints[1][1].split(':')
+            if len(endpoint_src_split) != 2 and len(endpoint_dst_split) != 2: return []
+            node_id_src = endpoint_src_split[0]
+            tp_id_src = endpoint_src_split[1]
+            node_id_dst = endpoint_dst_split[0]
+            tp_id_dst = endpoint_dst_split[1]
+        except ValueError:
+            return []
+
+        results = []
+        try:
+            device_uuid = endpoints[0][0]
+            db_device : DeviceModel = get_object(self.__database, DeviceModel, device_uuid, raise_if_not_found=True)
+            json_device = db_device.dump(include_config_rules=False, include_drivers=True, include_endpoints=True)
+            json_device_config : Dict = json_device.setdefault('device_config', {})
+            json_device_config_rules : List = json_device_config.setdefault('config_rules', [])
+            json_device_config_rules.extend([
+                config_rule_set('/service[{:s}]'.format(service_uuid), {
+                    'uuid'                    : service_uuid,
+                    'node_id_src'             : node_id_src,
+                    'tp_id_src'               : tp_id_src,
+                    'node_id_dst'             : node_id_dst,
+                    'tp_id_dst'               : tp_id_dst,
+                    'vlan_id'                 : vlan_id,
+                }),
+            ])
+            self.__device_client.ConfigureDevice(Device(**json_device))
+            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)
+
+        return results
+
+    def DeleteEndpoint(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> List[Union[bool, Exception]]:
+        chk_type('endpoints', endpoints, list)
+        if len(endpoints) != 2: return []
+
+        service_uuid = self.__db_service.service_uuid
+        results = []
+        try:
+            device_uuid = endpoints[0][0]
+            db_device : DeviceModel = get_object(self.__database, DeviceModel, device_uuid, raise_if_not_found=True)
+            json_device = db_device.dump(include_config_rules=False, include_drivers=True, include_endpoints=True)
+            json_device_config : Dict = json_device.setdefault('device_config', {})
+            json_device_config_rules : List = json_device_config.setdefault('config_rules', [])
+            json_device_config_rules.extend([
+                config_rule_delete('/service[{:s}]'.format(service_uuid), {'uuid': service_uuid})
+            ])
+            self.__device_client.ConfigureDevice(Device(**json_device))
+            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)
+
+        return results
+
+    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))]
+
+    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))]
+
+    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_key, resource_value = resource
+                resource_value = json.loads(resource_value)
+                set_subnode_value(self.__resolver, self.__config, resource_key, 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
+
+    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:
+                resource_key, _ = resource
+                delete_subnode(self.__resolver, self.__config, resource_key)
+            except Exception as e: # pylint: disable=broad-except
+                LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource)))
+                results.append(e)
+
+        return results
-- 
GitLab


From 4a3028d2a1aea96d74f1781af970b0742aa71448 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Mon, 21 Nov 2022 17:07:56 +0100
Subject: [PATCH 02/11] Device Driver MicroWave:

- minor improvement in error checking
---
 src/device/service/drivers/microwave/Tools.py | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/src/device/service/drivers/microwave/Tools.py b/src/device/service/drivers/microwave/Tools.py
index 93498f72d..4f74def4d 100644
--- a/src/device/service/drivers/microwave/Tools.py
+++ b/src/device/service/drivers/microwave/Tools.py
@@ -17,6 +17,12 @@ from device.service.driver_api._Driver import RESOURCE_ENDPOINTS
 
 LOGGER = logging.getLogger(__name__)
 
+HTTP_OK_CODES = {
+    200,    # OK
+    201,    # Created
+    202,    # Accepted
+    204,    # No Content
+}
 
 def find_key(resource, key):
     return json.loads(resource[1])[key]
@@ -128,10 +134,10 @@ def create_connectivity_service(
         LOGGER.exception('Exception creating ConnectivityService(uuid={:s}, data={:s})'.format(str(uuid), str(data)))
         results.append(e)
     else:
-        if response.status_code != 201:
+        if response.status_code not in HTTP_OK_CODES:
             msg = 'Could not create ConnectivityService(uuid={:s}, data={:s}). status_code={:s} reply={:s}'
             LOGGER.error(msg.format(str(uuid), str(data), str(response.status_code), str(response)))
-        results.append(response.status_code == 201)
+        results.append(response.status_code in HTTP_OK_CODES)
     return results
 
 def delete_connectivity_service(root_url, timeout, uuid):
@@ -144,8 +150,8 @@ def delete_connectivity_service(root_url, timeout, uuid):
         LOGGER.exception('Exception deleting ConnectivityService(uuid={:s})'.format(str(uuid)))
         results.append(e)
     else:
-        if response.status_code != 201:
+        if response.status_code not in HTTP_OK_CODES:
             msg = 'Could not delete ConnectivityService(uuid={:s}). status_code={:s} reply={:s}'
             LOGGER.error(msg.format(str(uuid), str(response.status_code), str(response)))
-        results.append(response.status_code == 202)
+        results.append(response.status_code in HTTP_OK_CODES)
     return results
-- 
GitLab


From b0be7852c310fcc78b906e12e25890baf9033740 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Mon, 21 Nov 2022 17:09:20 +0100
Subject: [PATCH 03/11] Service - MicroWaveServiceHandler:

- migrated to new ServiceHandler API
- added missing __init__.py file
---
 .../service/service_handlers/__init__.py      |   4 +-
 .../microwave/MicrowaveServiceHandler.py      | 103 ++++++++----------
 .../service_handlers/microwave/__init__.py    |  14 +++
 3 files changed, 64 insertions(+), 57 deletions(-)
 create mode 100644 src/service/service/service_handlers/microwave/__init__.py

diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py
index 6c3231b46..c6cb589d5 100644
--- a/src/service/service/service_handlers/__init__.py
+++ b/src/service/service/service_handlers/__init__.py
@@ -47,8 +47,8 @@ SERVICE_HANDLERS = [
     ]),
     (MicrowaveServiceHandler, [
         {
-            FilterFieldEnum.SERVICE_TYPE  : ORM_ServiceTypeEnum.L2NM,
-            FilterFieldEnum.DEVICE_DRIVER : ORM_DeviceDriverEnum.IETF_NETWORK_TOPOLOGY,
+            FilterFieldEnum.SERVICE_TYPE  : ServiceTypeEnum.SERVICETYPE_L2NM,
+            FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY,
         }
     ]),
 ]
diff --git a/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
index 6ac58e303..1fe59db2b 100644
--- a/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
+++ b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
@@ -14,57 +14,51 @@
 
 import anytree, json, logging
 from typing import Any, Dict, List, Optional, Tuple, Union
-from common.orm.Database import Database
-from common.orm.HighLevel import get_object
-from common.orm.backend.Tools import key_to_str
+from common.proto.context_pb2 import ConfigActionEnum, ConfigRule, DeviceId, Service
+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 context.client.ContextClient import ContextClient
-from device.client.DeviceClient import DeviceClient
-from device.proto.context_pb2 import Device
-from service.service.database.ConfigModel import ORM_ConfigActionEnum, get_config_rules
-from service.service.database.ContextModel import ContextModel
-from service.service.database.DeviceModel import DeviceModel
-from service.service.database.ServiceModel import ServiceModel
 from service.service.service_handler_api._ServiceHandler import _ServiceHandler
 from service.service.service_handler_api.AnyTreeTools import TreeNode, delete_subnode, get_subnode, set_subnode_value
-from service.service.service_handlers.Tools import config_rule_set, config_rule_delete
+from service.service.task_scheduler.TaskExecutor import TaskExecutor
 
 LOGGER = logging.getLogger(__name__)
 
 class MicrowaveServiceHandler(_ServiceHandler):
     def __init__(   # pylint: disable=super-init-not-called
-        self, db_service : ServiceModel, database : Database, context_client : ContextClient,
-        device_client : DeviceClient, **settings
+        self, service : Service, task_executor : TaskExecutor, **settings
     ) -> None:
-        self.__db_service = db_service
-        self.__database = database
-        self.__context_client = context_client # pylint: disable=unused-private-member
-        self.__device_client = device_client
-
-        self.__db_context : ContextModel = get_object(self.__database, ContextModel, self.__db_service.context_fk)
-        str_service_key = key_to_str([self.__db_context.context_uuid, self.__db_service.service_uuid])
-        db_config = get_config_rules(self.__database, str_service_key, 'running')
+        self.__service = service
+        self.__task_executor = task_executor # pylint: disable=unused-private-member
         self.__resolver = anytree.Resolver(pathattr='name')
         self.__config = TreeNode('.')
-        for action, resource_key, resource_value in db_config:
-            if action == ORM_ConfigActionEnum.SET:
+        for config_rule in service.service_config.config_rules:
+            action = config_rule.action
+            if config_rule.WhichOneof('config_rule') != 'custom': continue
+            resource_key = config_rule.custom.resource_key
+            resource_value = config_rule.custom.resource_value
+            if action == ConfigActionEnum.CONFIGACTION_SET:
                 try:
                     resource_value = json.loads(resource_value)
                 except: # pylint: disable=bare-except
                     pass
                 set_subnode_value(self.__resolver, self.__config, resource_key, resource_value)
-            elif action == ORM_ConfigActionEnum.DELETE:
+            elif action == ConfigActionEnum.CONFIGACTION_DELETE:
                 delete_subnode(self.__resolver, self.__config, resource_key)
 
-    def SetEndpoint(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> List[Union[bool, Exception]]:
+    def SetEndpoint(
+        self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None
+    ) -> List[Union[bool, Exception]]:
+        LOGGER.info('[SetEndpoint] endpoints={:s}'.format(str(endpoints)))
+        LOGGER.info('[SetEndpoint] connection_uuid={:s}'.format(str(connection_uuid)))
         chk_type('endpoints', endpoints, list)
         if len(endpoints) != 2: return []
 
-        service_uuid = self.__db_service.service_uuid
-        service_settings : TreeNode = get_subnode(self.__resolver, self.__config, 'settings', None)
-        if service_settings is None: raise Exception('Unable to settings for Service({:s})'.format(str(service_uuid)))
+        service_uuid = self.__service.service_id.service_uuid.uuid
+        settings : TreeNode = get_subnode(self.__resolver, self.__config, '/settings', None)
+        if settings is None: raise Exception('Unable to retrieve settings for Service({:s})'.format(str(service_uuid)))
 
-        json_settings : Dict = service_settings.value
+        json_settings : Dict = settings.value
         vlan_id = json_settings.get('vlan_id', 121)
         # endpoints are retrieved in the following format --> '/endpoints/endpoint[172.26.60.243:9]'
         try:
@@ -81,44 +75,43 @@ class MicrowaveServiceHandler(_ServiceHandler):
         results = []
         try:
             device_uuid = endpoints[0][0]
-            db_device : DeviceModel = get_object(self.__database, DeviceModel, device_uuid, raise_if_not_found=True)
-            json_device = db_device.dump(include_config_rules=False, include_drivers=True, include_endpoints=True)
-            json_device_config : Dict = json_device.setdefault('device_config', {})
-            json_device_config_rules : List = json_device_config.setdefault('config_rules', [])
-            json_device_config_rules.extend([
-                config_rule_set('/service[{:s}]'.format(service_uuid), {
-                    'uuid'                    : service_uuid,
-                    'node_id_src'             : node_id_src,
-                    'tp_id_src'               : tp_id_src,
-                    'node_id_dst'             : node_id_dst,
-                    'tp_id_dst'               : tp_id_dst,
-                    'vlan_id'                 : vlan_id,
-                }),
-            ])
-            self.__device_client.ConfigureDevice(Device(**json_device))
+            device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid)))
+            json_config_rule = json_config_rule_set('/service[{:s}]'.format(service_uuid), {
+                'uuid'       : service_uuid,
+                'node_id_src': node_id_src,
+                'tp_id_src'  : tp_id_src,
+                'node_id_dst': node_id_dst,
+                'tp_id_dst'  : tp_id_dst,
+                'vlan_id'    : vlan_id,
+            })
+            del device.device_config.config_rules[:]
+            device.device_config.config_rules.append(ConfigRule(**json_config_rule))
+            self.__task_executor.configure_device(device)
             results.append(True)
         except Exception as e: # pylint: disable=broad-except
-            LOGGER.exception('Unable to SetEndpoint for Service({:s})'.format(str(service_uuid)))
+            LOGGER.exception('Unable to configure Service({:s})'.format(str(service_uuid)))
             results.append(e)
 
         return results
 
-    def DeleteEndpoint(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> List[Union[bool, Exception]]:
+    def DeleteEndpoint(
+        self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None
+    ) -> List[Union[bool, Exception]]:
+        LOGGER.info('[DeleteEndpoint] endpoints={:s}'.format(str(endpoints)))
+        LOGGER.info('[DeleteEndpoint] connection_uuid={:s}'.format(str(connection_uuid)))
+
         chk_type('endpoints', endpoints, list)
         if len(endpoints) != 2: return []
 
-        service_uuid = self.__db_service.service_uuid
+        service_uuid = self.__service.service_id.service_uuid.uuid
         results = []
         try:
             device_uuid = endpoints[0][0]
-            db_device : DeviceModel = get_object(self.__database, DeviceModel, device_uuid, raise_if_not_found=True)
-            json_device = db_device.dump(include_config_rules=False, include_drivers=True, include_endpoints=True)
-            json_device_config : Dict = json_device.setdefault('device_config', {})
-            json_device_config_rules : List = json_device_config.setdefault('config_rules', [])
-            json_device_config_rules.extend([
-                config_rule_delete('/service[{:s}]'.format(service_uuid), {'uuid': service_uuid})
-            ])
-            self.__device_client.ConfigureDevice(Device(**json_device))
+            device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid)))
+            json_config_rule = json_config_rule_delete('/service[{:s}]'.format(service_uuid), {'uuid': service_uuid})
+            del device.device_config.config_rules[:]
+            device.device_config.config_rules.append(ConfigRule(**json_config_rule))
+            self.__task_executor.configure_device(device)
             results.append(True)
         except Exception as e: # pylint: disable=broad-except
             LOGGER.exception('Unable to DeleteEndpoint for Service({:s})'.format(str(service_uuid)))
diff --git a/src/service/service/service_handlers/microwave/__init__.py b/src/service/service/service_handlers/microwave/__init__.py
new file mode 100644
index 000000000..70a332512
--- /dev/null
+++ b/src/service/service/service_handlers/microwave/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
+#
+# 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 ed544a4afc290bb38f94d93542e8eca9e13dd679 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Mon, 21 Nov 2022 18:04:38 +0100
Subject: [PATCH 04/11] Common & PathComp:

- corrected device type name typo
- added missing devicetype-to-layer rules
---
 src/common/DeviceTypes.py                     |  4 +--
 src/common/tools/object_factory/Device.py     |  3 +-
 src/device/service/drivers/__init__.py        |  8 ++---
 .../algorithms/tools/ConstantsMappings.py     | 36 ++++++++++---------
 4 files changed, 27 insertions(+), 24 deletions(-)

diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py
index 08f18dd40..5bc16dd3e 100644
--- a/src/common/DeviceTypes.py
+++ b/src/common/DeviceTypes.py
@@ -18,7 +18,7 @@ class DeviceTypeEnum(Enum):
 
     # Emulated device types
     EMULATED_DATACENTER             = 'emu-datacenter'
-    EMULATED_MICROVAWE_RADIO_SYSTEM = 'emu-microwave-radio-system'
+    EMULATED_MICROWAVE_RADIO_SYSTEM = 'emu-microwave-radio-system'
     EMULATED_OPEN_LINE_SYSTEM       = 'emu-open-line-system'
     EMULATED_OPTICAL_ROADM          = 'emu-optical-roadm'
     EMULATED_OPTICAL_TRANSPONDER    = 'emu-optical-transponder'
@@ -28,7 +28,7 @@ class DeviceTypeEnum(Enum):
 
     # Real device types
     DATACENTER                      = 'datacenter'
-    MICROVAWE_RADIO_SYSTEM          = 'microwave-radio-system'
+    MICROWAVE_RADIO_SYSTEM          = 'microwave-radio-system'
     OPEN_LINE_SYSTEM                = 'open-line-system'
     OPTICAL_ROADM                   = 'optical-roadm'
     OPTICAL_TRANSPONDER             = 'optical-transponder'
diff --git a/src/common/tools/object_factory/Device.py b/src/common/tools/object_factory/Device.py
index 4a590134d..406af80a8 100644
--- a/src/common/tools/object_factory/Device.py
+++ b/src/common/tools/object_factory/Device.py
@@ -33,8 +33,7 @@ DEVICE_PR_DRIVERS   = [DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG]
 DEVICE_TAPI_TYPE    = DeviceTypeEnum.OPEN_LINE_SYSTEM.value
 DEVICE_TAPI_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API]
 
-# check which enum type and value assign to microwave device
-DEVICE_MICROWAVE_TYPE    = DeviceTypeEnum.MICROVAWE_RADIO_SYSTEM.value
+DEVICE_MICROWAVE_TYPE    = DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value
 DEVICE_MICROWAVE_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY]
 
 DEVICE_P4_TYPE      = DeviceTypeEnum.P4_SWITCH.value
diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py
index 535b553a8..3a56420c9 100644
--- a/src/device/service/drivers/__init__.py
+++ b/src/device/service/drivers/__init__.py
@@ -29,7 +29,7 @@ DRIVERS.append(
         {
             FilterFieldEnum.DEVICE_TYPE: [
                 DeviceTypeEnum.EMULATED_DATACENTER,
-                DeviceTypeEnum.EMULATED_MICROVAWE_RADIO_SYSTEM,
+                DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM,
                 DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM,
                 DeviceTypeEnum.EMULATED_OPTICAL_ROADM,
                 DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER,
@@ -38,7 +38,7 @@ DRIVERS.append(
                 DeviceTypeEnum.EMULATED_PACKET_SWITCH,
 
                 #DeviceTypeEnum.DATACENTER,
-                #DeviceTypeEnum.MICROVAWE_RADIO_SYSTEM,
+                #DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM,
                 #DeviceTypeEnum.OPEN_LINE_SYSTEM,
                 #DeviceTypeEnum.OPTICAL_ROADM,
                 #DeviceTypeEnum.OPTICAL_TRANSPONDER,
@@ -54,7 +54,7 @@ DRIVERS.append(
         #    # Emulated devices, all drivers => use Emulated
         #    FilterFieldEnum.DEVICE_TYPE: [
         #        DeviceTypeEnum.EMULATED_DATACENTER,
-        #        DeviceTypeEnum.EMULATED_MICROVAWE_RADIO_SYSTEM,
+        #        DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM,
         #        DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM,
         #        DeviceTypeEnum.EMULATED_OPTICAL_ROADM,
         #        DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER,
@@ -111,7 +111,7 @@ if LOAD_ALL_DEVICE_DRIVERS:
     DRIVERS.append(
         (IETFApiDriver, [
             {
-                FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.MICROVAWE_RADIO_SYSTEM,
+                FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM,
                 FilterFieldEnum.DRIVER     : ORM_DeviceDriverEnum.IETF_NETWORK_TOPOLOGY,
             }
         ]))
diff --git a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
index 8561ab110..332d38fd4 100644
--- a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
+++ b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
@@ -78,22 +78,26 @@ class DeviceLayerEnum(IntEnum):
     OPTICAL_DEVICE         =  0     # Layer 0 domain device
 
 DEVICE_TYPE_TO_LAYER = {
-    DeviceTypeEnum.EMULATED_DATACENTER.value      : DeviceLayerEnum.APPLICATION_DEVICE,
-    DeviceTypeEnum.DATACENTER.value               : DeviceLayerEnum.APPLICATION_DEVICE,
-
-    DeviceTypeEnum.EMULATED_PACKET_ROUTER.value   : DeviceLayerEnum.PACKET_DEVICE,
-    DeviceTypeEnum.PACKET_ROUTER.value            : DeviceLayerEnum.PACKET_DEVICE,
-    DeviceTypeEnum.EMULATED_PACKET_SWITCH.value   : DeviceLayerEnum.MAC_LAYER_DEVICE,
-    DeviceTypeEnum.PACKET_SWITCH.value            : DeviceLayerEnum.MAC_LAYER_DEVICE,
-    DeviceTypeEnum.P4_SWITCH.value                : DeviceLayerEnum.MAC_LAYER_DEVICE,
-
-    DeviceTypeEnum.MICROVAWE_RADIO_SYSTEM.value   : DeviceLayerEnum.MAC_LAYER_CONTROLLER,
-
-    DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value: DeviceLayerEnum.OPTICAL_CONTROLLER,
-    DeviceTypeEnum.OPEN_LINE_SYSTEM.value         : DeviceLayerEnum.OPTICAL_CONTROLLER,
-
-    DeviceTypeEnum.OPTICAL_ROADM.value            : DeviceLayerEnum.OPTICAL_DEVICE,
-    DeviceTypeEnum.OPTICAL_TRANSPONDER.value      : DeviceLayerEnum.OPTICAL_DEVICE,
+    DeviceTypeEnum.EMULATED_DATACENTER.value             : DeviceLayerEnum.APPLICATION_DEVICE,
+    DeviceTypeEnum.DATACENTER.value                      : DeviceLayerEnum.APPLICATION_DEVICE,
+
+    DeviceTypeEnum.EMULATED_PACKET_ROUTER.value          : DeviceLayerEnum.PACKET_DEVICE,
+    DeviceTypeEnum.PACKET_ROUTER.value                   : DeviceLayerEnum.PACKET_DEVICE,
+    DeviceTypeEnum.EMULATED_PACKET_SWITCH.value          : DeviceLayerEnum.MAC_LAYER_DEVICE,
+    DeviceTypeEnum.PACKET_SWITCH.value                   : DeviceLayerEnum.MAC_LAYER_DEVICE,
+    DeviceTypeEnum.EMULATED_P4_SWITCH.value              : DeviceLayerEnum.MAC_LAYER_DEVICE,
+    DeviceTypeEnum.P4_SWITCH.value                       : DeviceLayerEnum.MAC_LAYER_DEVICE,
+
+    DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value : DeviceLayerEnum.MAC_LAYER_CONTROLLER,
+    DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value          : DeviceLayerEnum.MAC_LAYER_CONTROLLER,
+
+    DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value       : DeviceLayerEnum.OPTICAL_CONTROLLER,
+    DeviceTypeEnum.OPEN_LINE_SYSTEM.value                : DeviceLayerEnum.OPTICAL_CONTROLLER,
+
+    DeviceTypeEnum.EMULATED_OPTICAL_ROADM.value          : DeviceLayerEnum.OPTICAL_DEVICE,
+    DeviceTypeEnum.OPTICAL_ROADM.value                   : DeviceLayerEnum.OPTICAL_DEVICE,
+    DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER.value    : DeviceLayerEnum.OPTICAL_DEVICE,
+    DeviceTypeEnum.OPTICAL_TRANSPONDER.value             : DeviceLayerEnum.OPTICAL_DEVICE,
 }
 
 DEVICE_LAYER_TO_SERVICE_TYPE = {
-- 
GitLab


From 9f4826d7d0dc230b07a19896954270cd84ca0a9e Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 11:58:50 +0100
Subject: [PATCH 05/11] WebUI component:

- added missing node icon for microwave radio systems (by now as a virtual controller)
---
 .../static/topology_icons/Acknowledgements.txt  |   4 ++++
 .../emu-microwave-radio-system.png              | Bin 0 -> 12869 bytes
 .../topology_icons/microwave-radio-system.png   | Bin 0 -> 13777 bytes
 3 files changed, 4 insertions(+)
 create mode 100644 src/webui/service/static/topology_icons/emu-microwave-radio-system.png
 create mode 100644 src/webui/service/static/topology_icons/microwave-radio-system.png

diff --git a/src/webui/service/static/topology_icons/Acknowledgements.txt b/src/webui/service/static/topology_icons/Acknowledgements.txt
index ddf7a8d0d..df5d16dc7 100644
--- a/src/webui/service/static/topology_icons/Acknowledgements.txt
+++ b/src/webui/service/static/topology_icons/Acknowledgements.txt
@@ -11,6 +11,10 @@ https://symbols.getvecta.com/stencil_241/224_router.be30fb87e7.png => emu-packet
 https://symbols.getvecta.com/stencil_240/269_virtual-layer-switch.ed10fdede6.png => open-line-system.png
 https://symbols.getvecta.com/stencil_241/281_virtual-layer-switch.29420aff2f.png => emu-open-line-system.png
 
+# Temporal icon; to be updated
+https://symbols.getvecta.com/stencil_240/269_virtual-layer-switch.ed10fdede6.png => microwave-radio-system.png
+https://symbols.getvecta.com/stencil_241/281_virtual-layer-switch.29420aff2f.png => emu-microwave-radio-system.png
+
 https://symbols.getvecta.com/stencil_240/102_ibm-tower.2cc133f3d0.png => datacenter.png
 https://symbols.getvecta.com/stencil_241/133_ibm-tower.995c44696c.png => emu-datacenter.png
 
diff --git a/src/webui/service/static/topology_icons/emu-microwave-radio-system.png b/src/webui/service/static/topology_icons/emu-microwave-radio-system.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5c30d679170c6e080dee3cc5239bf7ecaefe743
GIT binary patch
literal 12869
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
Xyi~U~2mVO}4^Kl?SEW`N750As2d5jB

literal 0
HcmV?d00001

diff --git a/src/webui/service/static/topology_icons/microwave-radio-system.png b/src/webui/service/static/topology_icons/microwave-radio-system.png
new file mode 100644
index 0000000000000000000000000000000000000000..b51f094216755ed9fc5c7a7e8957bab88090c954
GIT binary patch
literal 13777
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
NsVZqIR>@mN{6A11CyD?7

literal 0
HcmV?d00001

-- 
GitLab


From 42c31e2e10ab0c5e9be857097ceade82d8e78606 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 11:59:21 +0100
Subject: [PATCH 06/11] PathComp Frontend component:

- added device layer to service type mapping for MAC layer controllers
---
 .../service/algorithms/tools/ConstantsMappings.py      | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
index 332d38fd4..5e9b9a893 100644
--- a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
+++ b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py
@@ -101,10 +101,12 @@ DEVICE_TYPE_TO_LAYER = {
 }
 
 DEVICE_LAYER_TO_SERVICE_TYPE = {
-    DeviceLayerEnum.APPLICATION_DEVICE.value: ServiceTypeEnum.SERVICETYPE_L3NM,
+    DeviceLayerEnum.APPLICATION_DEVICE.value   : ServiceTypeEnum.SERVICETYPE_L3NM,
+    DeviceLayerEnum.PACKET_DEVICE.value        : ServiceTypeEnum.SERVICETYPE_L3NM,
 
-    DeviceLayerEnum.PACKET_DEVICE.value     : ServiceTypeEnum.SERVICETYPE_L3NM,
-    DeviceLayerEnum.MAC_LAYER_DEVICE.value  : ServiceTypeEnum.SERVICETYPE_L2NM,
+    DeviceLayerEnum.MAC_LAYER_CONTROLLER.value : ServiceTypeEnum.SERVICETYPE_L2NM,
+    DeviceLayerEnum.MAC_LAYER_DEVICE.value     : ServiceTypeEnum.SERVICETYPE_L2NM,
 
-    DeviceLayerEnum.OPTICAL_CONTROLLER.value: ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
+    DeviceLayerEnum.OPTICAL_CONTROLLER.value   : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
+    DeviceLayerEnum.OPTICAL_DEVICE.value       : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
 }
-- 
GitLab


From 8b72fc48fb706ed7f8cf3d67cf71844502aa3b71 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 15:29:25 +0100
Subject: [PATCH 07/11] Test tools:

- Descriptors for a test topology
- Descriptiors for a test microwave service
- Implemented Mock MicroWave SDN Controller to test MicroWaveDeviceDriver
---
 .../tools/mock_sdn_ctrl/MockMWSdnCtrl.py      | 130 ++++++++++++++++++
 .../tools/mock_sdn_ctrl/microwave_deploy.sh   |  22 +++
 .../mock_sdn_ctrl/network_descriptors.json    | 117 ++++++++++++++++
 .../mock_sdn_ctrl/service_descriptor.json     |  25 ++++
 4 files changed, 294 insertions(+)
 create mode 100644 src/tests/tools/mock_sdn_ctrl/MockMWSdnCtrl.py
 create mode 100644 src/tests/tools/mock_sdn_ctrl/microwave_deploy.sh
 create mode 100644 src/tests/tools/mock_sdn_ctrl/network_descriptors.json
 create mode 100644 src/tests/tools/mock_sdn_ctrl/service_descriptor.json

diff --git a/src/tests/tools/mock_sdn_ctrl/MockMWSdnCtrl.py b/src/tests/tools/mock_sdn_ctrl/MockMWSdnCtrl.py
new file mode 100644
index 000000000..61eec6fe6
--- /dev/null
+++ b/src/tests/tools/mock_sdn_ctrl/MockMWSdnCtrl.py
@@ -0,0 +1,130 @@
+# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
+#
+# 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.
+
+# Mock MicroWave SDN controller
+# -----------------------------
+# REST server implementing minimal support for:
+# - IETF YANG data model for Network Topology
+#       Ref: https://www.rfc-editor.org/rfc/rfc8345.html
+# - IETF YANG data model for Transport Network Client Signals
+#       Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-07.html
+
+
+# Ref: https://blog.miguelgrinberg.com/post/running-your-flask-application-over-https
+# Ref: https://blog.miguelgrinberg.com/post/designing-a-restful-api-using-flask-restful
+
+import functools, logging, sys, time
+from flask import Flask, abort, request
+from flask.json import jsonify
+from flask_restful import Api, Resource
+
+BIND_ADDRESS = '0.0.0.0'
+BIND_PORT    = 8443
+BASE_URL     = '/nmswebs/restconf/data'
+STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL))
+LOG_LEVEL    = logging.DEBUG
+
+NETWORK_NODES = [
+    {'node-id': '172.18.0.1', 'ietf-network-topology:termination-point': [
+        {'tp-id': '172.18.0.1:1', 'ietf-te-topology:te': {'name': 'ethernet'}},
+        {'tp-id': '172.18.0.1:2', 'ietf-te-topology:te': {'name': 'antena'  }},
+    ]},
+    {'node-id': '172.18.0.2', 'ietf-network-topology:termination-point': [
+        {'tp-id': '172.18.0.2:1', 'ietf-te-topology:te': {'name': 'ethernet'}},
+        {'tp-id': '172.18.0.2:2', 'ietf-te-topology:te': {'name': 'antena'  }},
+    ]},
+    {'node-id': '172.18.0.3', 'ietf-network-topology:termination-point': [
+        {'tp-id': '172.18.0.3:1', 'ietf-te-topology:te': {'name': 'ethernet'}},
+        {'tp-id': '172.18.0.3:2', 'ietf-te-topology:te': {'name': 'antena'  }},
+    ]},
+    {'node-id': '172.18.0.4', 'ietf-network-topology:termination-point': [
+        {'tp-id': '172.18.0.4:1', 'ietf-te-topology:te': {'name': 'ethernet'}},
+        {'tp-id': '172.18.0.4:2', 'ietf-te-topology:te': {'name': 'antena'  }},
+    ]}
+]
+NETWORK_LINKS = [
+    {
+        'source'     : {'source-node': '172.18.0.1', 'source-tp': '172.18.0.1:2'},
+        'destination': {'dest-node'  : '172.18.0.2', 'dest-tp'  : '172.18.0.2:2'},
+    },
+    {
+        'source'     : {'source-node': '172.18.0.3', 'source-tp': '172.18.0.3:2'},
+        'destination': {'dest-node'  : '172.18.0.4', 'dest-tp'  : '172.18.0.4:2'},
+    }
+]
+NETWORK_SERVICES = {}
+
+
+logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s")
+LOGGER = logging.getLogger(__name__)
+
+logging.getLogger('werkzeug').setLevel(logging.WARNING)
+
+def log_request(logger : logging.Logger, response):
+    timestamp = time.strftime('[%Y-%b-%d %H:%M]')
+    logger.info('%s %s %s %s %s', timestamp, request.remote_addr, request.method, request.full_path, response.status)
+    return response
+
+class Health(Resource):
+    def get(self): return jsonify({})
+
+class Network(Resource):
+    def get(self, network_uuid : str):
+        if network_uuid != 'SIAE-ETH-TOPOLOGY': abort(400)
+        network = {'node': NETWORK_NODES, 'ietf-network-topology:link': NETWORK_LINKS}
+        return jsonify({'ietf-network:network': network})
+
+class Services(Resource):
+    def get(self):
+        services = [service for service in NETWORK_SERVICES.values()]
+        return jsonify({'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': services}})
+
+    def post(self):
+        json_request = request.json
+        if not json_request: abort(400)
+        if not isinstance(json_request, dict): abort(400)
+        if 'etht-svc-instances' not in json_request: abort(400)
+        json_services = json_request['etht-svc-instances']
+        if not isinstance(json_services, list): abort(400)
+        if len(json_services) != 1: abort(400)
+        svc_data = json_services[0]
+        etht_svc_name = svc_data['etht-svc-name']
+        NETWORK_SERVICES[etht_svc_name] = svc_data
+        return jsonify({}), 201
+
+class DelServices(Resource):
+    def delete(self, service_uuid : str):
+        NETWORK_SERVICES.pop(service_uuid, None)
+        return jsonify({}), 204
+
+def main():
+    LOGGER.info('Starting...')
+    
+    app = Flask(__name__)
+    app.after_request(functools.partial(log_request, LOGGER))
+
+    api = Api(app, prefix=BASE_URL)
+    api.add_resource(Health,      '/ietf-network:networks')
+    api.add_resource(Network,     '/ietf-network:networks/network=<string:network_uuid>')
+    api.add_resource(Services,    '/ietf-eth-tran-service:etht-svc')
+    api.add_resource(DelServices, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=<string:service_uuid>')
+
+    LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT)))
+    app.run(debug=True, host=BIND_ADDRESS, port=BIND_PORT, ssl_context='adhoc')
+
+    LOGGER.info('Bye')
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/src/tests/tools/mock_sdn_ctrl/microwave_deploy.sh b/src/tests/tools/mock_sdn_ctrl/microwave_deploy.sh
new file mode 100644
index 000000000..2da884899
--- /dev/null
+++ b/src/tests/tools/mock_sdn_ctrl/microwave_deploy.sh
@@ -0,0 +1,22 @@
+# Set the URL of your local Docker registry where the images will be uploaded to.
+export TFS_REGISTRY_IMAGE="http://localhost:32000/tfs/"
+
+# Set the list of components, separated by spaces, you want to build images for, and deploy.
+# Supported components are:
+#   context device automation policy service compute monitoring webui
+#   interdomain slice pathcomp dlt
+#   dbscanserving opticalattackmitigator opticalattackdetector
+#   l3_attackmitigator l3_centralizedattackdetector l3_distributedattackdetector
+export TFS_COMPONENTS="context device pathcomp service slice webui"
+
+# Set the tag you want to use for your images.
+export TFS_IMAGE_TAG="dev"
+
+# Set the name of the Kubernetes namespace to deploy to.
+export TFS_K8S_NAMESPACE="tfs"
+
+# Set additional manifest files to be applied after the deployment
+export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml"
+
+# Set the neew Grafana admin password
+export TFS_GRAFANA_PASSWORD="admin123+"
diff --git a/src/tests/tools/mock_sdn_ctrl/network_descriptors.json b/src/tests/tools/mock_sdn_ctrl/network_descriptors.json
new file mode 100644
index 000000000..d42fe61dc
--- /dev/null
+++ b/src/tests/tools/mock_sdn_ctrl/network_descriptors.json
@@ -0,0 +1,117 @@
+{
+    "contexts": [
+        {
+            "context_id": {"context_uuid": {"uuid": "admin"}},
+            "topology_ids": [],
+            "service_ids": []
+        }
+    ],
+    "topologies": [
+        {
+            "topology_id": {"topology_uuid": {"uuid": "admin"}, "context_id": {"context_uuid": {"uuid": "admin"}}},
+            "device_ids": [],
+            "link_ids": []
+        }
+    ],
+    "devices": [
+        {
+            "device_id": {"device_uuid": {"uuid": "R1"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_operational_status": 2, "device_endpoints": [],
+            "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": [
+                    {"type": "copper", "uuid": "MW", "sample_types": []},
+                    {"type": "copper", "uuid": "R2", "sample_types": []},
+                    {"type": "copper", "uuid": "EXT", "sample_types": []}
+                ]}}}
+            ]}
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "R2"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_operational_status": 2, "device_endpoints": [],
+            "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": [
+                    {"type": "copper", "uuid": "MW", "sample_types": []},
+                    {"type": "copper", "uuid": "R1", "sample_types": []},
+                    {"type": "copper", "uuid": "EXT", "sample_types": []}
+                ]}}}
+            ]}
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "R3"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_operational_status": 2, "device_endpoints": [],
+            "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": [
+                    {"type": "copper", "uuid": "MW", "sample_types": []},
+                    {"type": "copper", "uuid": "R4", "sample_types": []},
+                    {"type": "copper", "uuid": "EXT", "sample_types": []}
+                ]}}}
+            ]}
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "R4"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_operational_status": 2, "device_endpoints": [],
+            "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": [
+                    {"type": "copper", "uuid": "MW", "sample_types": []},
+                    {"type": "copper", "uuid": "R3", "sample_types": []},
+                    {"type": "copper", "uuid": "EXT", "sample_types": []}
+                ]}}}
+            ]}
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "MW"}}, "device_type": "microwave-radio-system", "device_drivers": [4],
+            "device_operational_status": 2, "device_endpoints": [],
+            "device_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "192.168.1.56"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}},
+                {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"timeout": 120}}}
+            ]}
+        }
+    ],
+    "links": [
+        {
+            "link_id": {"link_uuid": {"uuid": "R1/R2==R2/R1"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "R2"}},
+                {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "R1"}}
+            ]
+        },
+        {
+            "link_id": {"link_uuid": {"uuid": "R3/R4==R4/R3"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "R4"}},
+                {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "R3"}}
+            ]
+        },
+        {
+            "link_id": {"link_uuid": {"uuid": "R1/MW==MW/172.18.0.1:1"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "MW"}},
+                {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.1:1"}}
+            ]
+        },
+        {
+            "link_id": {"link_uuid": {"uuid": "R2/MW==MW/172.18.0.2:1"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R2"}}, "endpoint_uuid": {"uuid": "MW"}},
+                {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.2:1"}}
+            ]
+        },
+        {
+            "link_id": {"link_uuid": {"uuid": "R3/MW==MW/172.18.0.3:1"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "MW"}},
+                {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.3:1"}}
+            ]
+        },
+        {
+            "link_id": {"link_uuid": {"uuid": "R4/MW==MW/172.18.0.4:1"}}, "link_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R4"}}, "endpoint_uuid": {"uuid": "MW"}},
+                {"device_id": {"device_uuid": {"uuid": "MW"}}, "endpoint_uuid": {"uuid": "172.18.0.4:1"}}
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/tests/tools/mock_sdn_ctrl/service_descriptor.json b/src/tests/tools/mock_sdn_ctrl/service_descriptor.json
new file mode 100644
index 000000000..3e15bed5c
--- /dev/null
+++ b/src/tests/tools/mock_sdn_ctrl/service_descriptor.json
@@ -0,0 +1,25 @@
+{
+    "services": [
+        {
+            "service_id": {
+                "context_id": {"context_uuid": {"uuid": "admin"}},
+                "service_uuid": {"uuid": "siae-svc"}
+            },
+            "service_type": 2,
+            "service_status": {"service_status": 1},
+            "service_endpoint_ids": [
+                {"device_id": {"device_uuid": {"uuid": "R1"}}, "endpoint_uuid": {"uuid": "EXT"}},
+                {"device_id": {"device_uuid": {"uuid": "R3"}}, "endpoint_uuid": {"uuid": "EXT"}}
+            ],
+            "service_constraints": [
+                {"custom": {"constraint_type": "bandwidth[gbps]", "constraint_value": "10.0"}},
+                {"custom": {"constraint_type": "latency[ms]", "constraint_value": "15.2"}}
+            ],
+            "service_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "/settings", "resource_value": {
+                    "vlan_id": 121
+                }}}
+            ]}
+        }
+    ]
+}
-- 
GitLab


From 6df7c1c16da73673a9963fa645dcc5e82f19b768 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 15:29:51 +0100
Subject: [PATCH 08/11] Manifests:

- Reduced log level of device, service and slice components to INFO
---
 manifests/deviceservice.yaml  | 2 +-
 manifests/serviceservice.yaml | 2 +-
 manifests/sliceservice.yaml   | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/manifests/deviceservice.yaml b/manifests/deviceservice.yaml
index 46c7557d9..171394f7c 100644
--- a/manifests/deviceservice.yaml
+++ b/manifests/deviceservice.yaml
@@ -34,7 +34,7 @@ spec:
         - containerPort: 2020
         env:
         - name: LOG_LEVEL
-          value: "DEBUG"
+          value: "INFO"
         readinessProbe:
           exec:
             command: ["/bin/grpc_health_probe", "-addr=:2020"]
diff --git a/manifests/serviceservice.yaml b/manifests/serviceservice.yaml
index efe43fe22..75832b94f 100644
--- a/manifests/serviceservice.yaml
+++ b/manifests/serviceservice.yaml
@@ -34,7 +34,7 @@ spec:
         - containerPort: 3030
         env:
         - name: LOG_LEVEL
-          value: "DEBUG"
+          value: "INFO"
         readinessProbe:
           exec:
             command: ["/bin/grpc_health_probe", "-addr=:3030"]
diff --git a/manifests/sliceservice.yaml b/manifests/sliceservice.yaml
index eeed3776c..8c76618a9 100644
--- a/manifests/sliceservice.yaml
+++ b/manifests/sliceservice.yaml
@@ -34,7 +34,7 @@ spec:
         - containerPort: 4040
         env:
         - name: LOG_LEVEL
-          value: "DEBUG"
+          value: "INFO"
         readinessProbe:
           exec:
             command: ["/bin/grpc_health_probe", "-addr=:4040"]
-- 
GitLab


From b481f48c62a7f3bbd1c1e91e11b4804c5a7af583 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 15:30:28 +0100
Subject: [PATCH 09/11] PathComputation component:

- added logic to propagate config rules of services to subservices by default
---
 .../frontend/service/algorithms/_Algorithm.py        | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py
index b798813a8..43811c068 100644
--- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py
+++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py
@@ -134,7 +134,8 @@ class _Algorithm:
 
     def add_service_to_reply(
         self, reply : PathCompReply, context_uuid : str, service_uuid : str,
-        device_layer : Optional[DeviceLayerEnum] = None, path_hops : List[Dict] = []
+        device_layer : Optional[DeviceLayerEnum] = None, path_hops : List[Dict] = [],
+        config_rules : List = []
     ) -> Service:
         # TODO: implement support for multi-point services
         # Control deactivated to enable disjoint paths with multiple redundant endpoints on each side
@@ -168,6 +169,8 @@ class _Algorithm:
                     }
                     config_rule = ConfigRule(**json_config_rule_set('/settings', json_tapi_settings))
                     service.service_config.config_rules.append(config_rule)
+                else:
+                    service.service_config.config_rules.extend(config_rules)
 
             service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED
 
@@ -192,7 +195,8 @@ class _Algorithm:
             context_uuid = service_id['contextId']
             service_uuid = service_id['service_uuid']
             service_key = (context_uuid, service_uuid)
-            grpc_services[service_key] = self.add_service_to_reply(reply, context_uuid, service_uuid)
+            upper_service = self.add_service_to_reply(reply, context_uuid, service_uuid)
+            grpc_services[service_key] = upper_service
 
             no_path_issue = response.get('noPath', {}).get('issue')
             if no_path_issue is not None:
@@ -209,8 +213,10 @@ class _Algorithm:
                     service_key = (context_uuid, connection_uuid)
                     grpc_service = grpc_services.get(service_key)
                     if grpc_service is None:
+                        config_rules = upper_service.service_config.config_rules
                         grpc_service = self.add_service_to_reply(
-                            reply, context_uuid, connection_uuid, device_layer=device_layer, path_hops=path_hops)
+                            reply, context_uuid, connection_uuid, device_layer=device_layer, path_hops=path_hops,
+                            config_rules=config_rules)
                         grpc_services[service_key] = grpc_service
 
                 for connection in connections:
-- 
GitLab


From 2ad66d5b59321ca1566df9732e32c2b741d4c387 Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 15:31:26 +0100
Subject: [PATCH 10/11] Device MicroWave Device Driver:

- aesthetic code formatting
- improved error checking
- factorized code
---
 .../microwave/MicrowaveServiceHandler.py      | 46 ++++++++++---------
 1 file changed, 24 insertions(+), 22 deletions(-)

diff --git a/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
index 1fe59db2b..1ae08bbf6 100644
--- a/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
+++ b/src/service/service/service_handlers/microwave/MicrowaveServiceHandler.py
@@ -24,6 +24,12 @@ from service.service.task_scheduler.TaskExecutor import TaskExecutor
 
 LOGGER = logging.getLogger(__name__)
 
+def check_endpoint(endpoint : str, service_uuid : str) -> Tuple[str, str]:
+    endpoint_split = endpoint.split(':')
+    if len(endpoint_split) != 2:
+        raise Exception('Endpoint({:s}) is malformed for Service({:s})'.format(str(endpoint), str(service_uuid)))
+    return endpoint_split
+
 class MicrowaveServiceHandler(_ServiceHandler):
     def __init__(   # pylint: disable=super-init-not-called
         self, service : Service, task_executor : TaskExecutor, **settings
@@ -51,29 +57,24 @@ class MicrowaveServiceHandler(_ServiceHandler):
     ) -> List[Union[bool, Exception]]:
         LOGGER.info('[SetEndpoint] endpoints={:s}'.format(str(endpoints)))
         LOGGER.info('[SetEndpoint] connection_uuid={:s}'.format(str(connection_uuid)))
-        chk_type('endpoints', endpoints, list)
-        if len(endpoints) != 2: return []
 
         service_uuid = self.__service.service_id.service_uuid.uuid
-        settings : TreeNode = get_subnode(self.__resolver, self.__config, '/settings', None)
-        if settings is None: raise Exception('Unable to retrieve settings for Service({:s})'.format(str(service_uuid)))
-
-        json_settings : Dict = settings.value
-        vlan_id = json_settings.get('vlan_id', 121)
-        # endpoints are retrieved in the following format --> '/endpoints/endpoint[172.26.60.243:9]'
-        try:
-            endpoint_src_split = endpoints[0][1].split(':')
-            endpoint_dst_split = endpoints[1][1].split(':')
-            if len(endpoint_src_split) != 2 and len(endpoint_dst_split) != 2: return []
-            node_id_src = endpoint_src_split[0]
-            tp_id_src = endpoint_src_split[1]
-            node_id_dst = endpoint_dst_split[0]
-            tp_id_dst = endpoint_dst_split[1]
-        except ValueError:
-            return []
 
         results = []
         try:
+            chk_type('endpoints', endpoints, list)
+            if len(endpoints) != 2: raise Exception('len(endpoints) != 2')
+
+            settings : TreeNode = get_subnode(self.__resolver, self.__config, '/settings', None)
+            if settings is None:
+                raise Exception('Unable to retrieve settings for Service({:s})'.format(str(service_uuid)))
+
+            json_settings : Dict = settings.value
+            vlan_id = json_settings.get('vlan_id', 121)
+            # endpoints are retrieved in the following format --> '/endpoints/endpoint[172.26.60.243:9]'
+            node_id_src, tp_id_src = check_endpoint(endpoints[0][1], service_uuid)
+            node_id_dst, tp_id_dst = check_endpoint(endpoints[1][1], service_uuid)
+        
             device_uuid = endpoints[0][0]
             device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid)))
             json_config_rule = json_config_rule_set('/service[{:s}]'.format(service_uuid), {
@@ -89,7 +90,7 @@ class MicrowaveServiceHandler(_ServiceHandler):
             self.__task_executor.configure_device(device)
             results.append(True)
         except Exception as e: # pylint: disable=broad-except
-            LOGGER.exception('Unable to configure Service({:s})'.format(str(service_uuid)))
+            LOGGER.exception('Unable to SetEndpoint for Service({:s})'.format(str(service_uuid)))
             results.append(e)
 
         return results
@@ -100,12 +101,13 @@ class MicrowaveServiceHandler(_ServiceHandler):
         LOGGER.info('[DeleteEndpoint] endpoints={:s}'.format(str(endpoints)))
         LOGGER.info('[DeleteEndpoint] connection_uuid={:s}'.format(str(connection_uuid)))
 
-        chk_type('endpoints', endpoints, list)
-        if len(endpoints) != 2: return []
-
         service_uuid = self.__service.service_id.service_uuid.uuid
+
         results = []
         try:
+            chk_type('endpoints', endpoints, list)
+            if len(endpoints) != 2: raise Exception('len(endpoints) != 2')
+
             device_uuid = endpoints[0][0]
             device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid)))
             json_config_rule = json_config_rule_delete('/service[{:s}]'.format(service_uuid), {'uuid': service_uuid})
-- 
GitLab


From 9e3381e8d0416bd5dace217ee1a130d5c431063b Mon Sep 17 00:00:00 2001
From: gifrerenom <lluis.gifre@cttc.es>
Date: Tue, 22 Nov 2022 16:07:49 +0100
Subject: [PATCH 11/11] Test tools:

- Added readme file on how to use the Mock MicroWave SDN Controller and run the tests for the MicroWaveDeviceDriver and MicroWaveServiceHandler.
- Updated the descriptors associated to this test.
---
 src/tests/tools/mock_sdn_ctrl/README.md       | 53 +++++++++++++++++++
 .../mock_sdn_ctrl/network_descriptors.json    |  2 +-
 .../mock_sdn_ctrl/service_descriptor.json     |  2 +-
 3 files changed, 55 insertions(+), 2 deletions(-)
 create mode 100644 src/tests/tools/mock_sdn_ctrl/README.md

diff --git a/src/tests/tools/mock_sdn_ctrl/README.md b/src/tests/tools/mock_sdn_ctrl/README.md
new file mode 100644
index 000000000..d8a6fe6b2
--- /dev/null
+++ b/src/tests/tools/mock_sdn_ctrl/README.md
@@ -0,0 +1,53 @@
+# Mock MicroWave SDN Controller
+
+This REST server implements very basic support for the following YANG data models:
+- IETF YANG data model for Network Topology
+  - Ref: https://www.rfc-editor.org/rfc/rfc8345.html
+- IETF YANG data model for Transport Network Client Signals
+  - Ref: https://www.ietf.org/archive/id/draft-ietf-ccamp-client-signal-yang-07.html
+
+The aim of this server is to enable testing the MicroWaveDeviceDriver and the MicroWaveServiceHandler.
+Follow the steps below to perform the test:
+
+## 1. Deploy TeraFlowSDN controller and the scenario
+Deploy the test scenario "microwave_deploy.sh":
+```bash
+source src/tests/tools/microwave_deploy.sh
+./deploy.sh
+```
+
+## 2. Install requirements and run the Mock MicroWave SDN controller
+__NOTE__: if you run the Mock MicroWave SDN controller from the PyEnv used for developping on the TeraFlowSDN framework,
+all the requirements are already in place. Install them only if you execute it in a separate/standalone environment.
+
+Install the required dependencies as follows:
+```bash
+pip install Flask==2.1.3 Flask-RESTful==0.3.9
+```
+
+Run the Mock MicroWave SDN Controller as follows:
+```bash
+python src/tests/tools/mock_sdn_ctrl/MockMWSdnCtrl.py
+```
+
+## 3. Deploy the test descriptors
+Edit the descriptors to meet your environment specifications.
+Edit "network_descriptors.json" and change IP address and port of the MicroWave SDN controller of the "MW" device.
+- Set value of config rule "_connect/address" to the address of the host where the Mock MicroWave SDN controller is
+  running (default="192.168.1.1").
+- Set value of config rule "_connect/port" to the port where your Mock MicroWave SDN controller is listening on
+  (default="8443").
+
+Upload the "network_descriptors.json" through the TeraFlowSDN WebUI.
+- If not already selected, select context "admin".
+- Check that a network topology with 4 routers + 1 microwave radio system are loaded. They should form 2 rings.
+
+Upload the "service_descriptor.json" through the TeraFlowSDN WebUI.
+- Check that 2 services have been created.
+- The "mw-svc" should have a connection and be supported by a sub-service.
+- The sub-service should also have a connection.
+- The R1, R3, and MW devices should have configuration rules established.
+
+# 4. Delete the microwave service
+Find the "mw-svc" on the WebUI, navigate to its details, and delete the service pressing the "Delete Service" button.
+The service, sub-service, and device configuration rules should be removed.
diff --git a/src/tests/tools/mock_sdn_ctrl/network_descriptors.json b/src/tests/tools/mock_sdn_ctrl/network_descriptors.json
index d42fe61dc..25fa940a4 100644
--- a/src/tests/tools/mock_sdn_ctrl/network_descriptors.json
+++ b/src/tests/tools/mock_sdn_ctrl/network_descriptors.json
@@ -70,7 +70,7 @@
             "device_id": {"device_uuid": {"uuid": "MW"}}, "device_type": "microwave-radio-system", "device_drivers": [4],
             "device_operational_status": 2, "device_endpoints": [],
             "device_config": {"config_rules": [
-                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "192.168.1.56"}},
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "192.168.1.1"}},
                 {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}},
                 {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"timeout": 120}}}
             ]}
diff --git a/src/tests/tools/mock_sdn_ctrl/service_descriptor.json b/src/tests/tools/mock_sdn_ctrl/service_descriptor.json
index 3e15bed5c..a4109bc7b 100644
--- a/src/tests/tools/mock_sdn_ctrl/service_descriptor.json
+++ b/src/tests/tools/mock_sdn_ctrl/service_descriptor.json
@@ -3,7 +3,7 @@
         {
             "service_id": {
                 "context_id": {"context_uuid": {"uuid": "admin"}},
-                "service_uuid": {"uuid": "siae-svc"}
+                "service_uuid": {"uuid": "mw-svc"}
             },
             "service_type": 2,
             "service_status": {"service_status": 1},
-- 
GitLab