diff --git a/my_deploy.sh b/my_deploy.sh
index 7dd5e5c3ee13cbce2b701b5b4e703823dfb2c28f..4e705622bce9d9642277aec35e51fc61f394ae61 100755
--- a/my_deploy.sh
+++ b/my_deploy.sh
@@ -29,7 +29,14 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_gene
 #export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker"
 
 # Uncomment to activate Optical Controller
-#export TFS_COMPONENTS="${TFS_COMPONENTS} opticalcontroller"
+#   To manage optical connections, "service" requires "opticalcontroller" to be deployed
+#   before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+#   "opticalcontroller" only if "service" is already in TFS_COMPONENTS, and re-export it.
+#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then
+#    BEFORE="${TFS_COMPONENTS% service*}"
+#    AFTER="${TFS_COMPONENTS#* service}"
+#    export TFS_COMPONENTS="${BEFORE} opticalcontroller service ${AFTER}"
+#fi
 
 # Uncomment to activate ZTP
 #export TFS_COMPONENTS="${TFS_COMPONENTS} ztp"
diff --git a/src/context/service/ContextServiceServicerImpl.py b/src/context/service/ContextServiceServicerImpl.py
index a102fa17629bd866d96883230a542a6e7a4d92ff..379705372be0cbe1050311f264dd04ffbe6b7656 100644
--- a/src/context/service/ContextServiceServicerImpl.py
+++ b/src/context/service/ContextServiceServicerImpl.py
@@ -305,7 +305,7 @@ class ContextServiceServicerImpl(ContextServiceServicer, ContextPolicyServiceSer
     @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
     def GetOpticalConfig(self, request : Empty, context : grpc.ServicerContext) -> OpticalConfigList:
         result = get_opticalconfig(self.db_engine)
-        return OpticalConfigList(OpticalConfigs=result)
+        return OpticalConfigList(opticalconfigs=result)
 
     @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
     def SetOpticalConfig(self, request : OpticalConfig, context : grpc.ServicerContext) -> OpticalConfigId:
@@ -315,6 +315,4 @@ class ContextServiceServicerImpl(ContextServiceServicer, ContextPolicyServiceSer
     @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
     def SelectOpticalConfig(self, request : OpticalConfigId, context : grpc.ServicerContext) -> OpticalConfig:
         result = select_opticalconfig(self.db_engine, request)
-        optical_config_id = OpticalConfigId()
-        optical_config_id.CopyFrom(result.OpticalConfig_id)
-        return OpticalConfig(config=result.config, OpticalConfig_id=optical_config_id)
+        return OpticalConfig(config=result.config, opticalconfig_id=result.opticalconfig_id)
diff --git a/src/opticalcontroller/OpticalController.py b/src/opticalcontroller/OpticalController.py
index c2805695a75933c73d4ad367176bee8b504d4460..326862f93453674d5dff5566c2184dba1f00565c 100644
--- a/src/opticalcontroller/OpticalController.py
+++ b/src/opticalcontroller/OpticalController.py
@@ -85,6 +85,7 @@ class AddFlexLightpath(Resource):
         if rsa is not None:
             flow_id, optical_band_id = rsa.rsa_fs_computation(src, dst, bitrate, bidir, band)
             print (f"flow_id {flow_id} and optical_band_id {optical_band_id} ")
+            LOGGER.debug('flow_id={:s} rsa.db_flows={:s}'.format(str(flow_id), str(rsa.db_flows)))
             if flow_id is not None:
                 if rsa.db_flows[flow_id]["op-mode"] == 0:
                     return 'No path found', 404
diff --git a/src/service/service/task_scheduler/TaskExecutor.py b/src/service/service/task_scheduler/TaskExecutor.py
index 5c5747970c6accf3c76da6d4ffb60de48edbcdac..fdfcb903b59e4780e1c66ba3a9ee698bf4bea6f9 100644
--- a/src/service/service/task_scheduler/TaskExecutor.py
+++ b/src/service/service/task_scheduler/TaskExecutor.py
@@ -122,17 +122,16 @@ class TaskExecutor:
         optical_config_id = OpticalConfigId()
         optical_config_id.opticalconfig_uuid = device.device_id.device_uuid.uuid
         optical_config = OpticalConfig()
-        setting = settings.value if settings else ""
+        setting = settings.value if settings else ''
 
-        new_config = {}
         try:
             result = self._context_client.SelectOpticalConfig(optical_config_id)
-            new_config = json.loads(result.config)
-            if result is not None :
+            if result is not None:
+                new_config = json.loads(result.config)
                 new_config["new_config"] = setting
                 new_config["is_opticalband"] = is_opticalband
                 new_config["flow"] = flows
-                result.config = str(new_config)
+                result.config = json.dumps(new_config)
                 optical_config.CopyFrom(result)
                 self._device_client.ConfigureOpticalDevice(optical_config)
             self._store_grpc_object(CacheableObjectType.DEVICE, device_key, device)
diff --git a/src/service/service/tools/OpticalTools.py b/src/service/service/tools/OpticalTools.py
index 20652437194b9ef498f5b83bbe996863ba49c911..1837bf688c3ab4f61d49c4f481928896a6f4dbd7 100644
--- a/src/service/service/tools/OpticalTools.py
+++ b/src/service/service/tools/OpticalTools.py
@@ -13,35 +13,58 @@
 # limitations under the License.
 # 
 
-import json
-import requests
-import uuid
-from common.Constants import *
+import functools, json, logging, requests, uuid
 from typing import List
+from common.Constants import ServiceNameEnum
 from common.proto.context_pb2 import(
     Device, DeviceId, Service, Connection, EndPointId, TopologyId, ContextId, Uuid,
     ConfigRule, ConfigActionEnum, ConfigRule_Custom
 )
 from common.proto.pathcomp_pb2 import PathCompReply
 from common.Settings import (
-    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, find_environment_variables, get_env_var_name
+    ENVVAR_SUFIX_SERVICE_BASEURL_HTTP, ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC,
+    find_environment_variables, get_env_var_name
+)
+from service.service.tools.replies import (
+    reply_uni_txt, optical_band_uni_txt, reply_bid_txt, optical_band_bid_txt
 )
-from service.service.tools.replies import reply_uni_txt, optical_band_uni_txt, reply_bid_txt, optical_band_bid_txt
 
 log = logging.getLogger(__name__)
 
-testing = False
+TESTING = False
+
+get_optical_controller_setting = functools.partial(get_env_var_name, ServiceNameEnum.OPTICALCONTROLLER)
+VAR_NAME_OPTICAL_CTRL_BASEURL_HTTP = get_optical_controller_setting(ENVVAR_SUFIX_SERVICE_BASEURL_HTTP)
+VAR_NAME_OPTICAL_CTRL_SCHEMA       = get_optical_controller_setting('SCHEMA')
+VAR_NAME_OPTICAL_CTRL_HOST         = get_optical_controller_setting(ENVVAR_SUFIX_SERVICE_HOST)
+VAR_NAME_OPTICAL_CTRL_PORT         = get_optical_controller_setting(ENVVAR_SUFIX_SERVICE_PORT_GRPC)
+
+OPTICAL_CTRL_BASE_URL = '{:s}://{:s}:{:d}/OpticalTFS'
+
+def get_optical_controller_base_url() -> str:
+    settings = find_environment_variables([
+        VAR_NAME_OPTICAL_CTRL_BASEURL_HTTP,
+        VAR_NAME_OPTICAL_CTRL_SCHEMA,
+        VAR_NAME_OPTICAL_CTRL_HOST,
+        VAR_NAME_OPTICAL_CTRL_PORT,
+    ])
+    base_url = settings.get(VAR_NAME_OPTICAL_CTRL_BASEURL_HTTP)
+    if base_url is not None:
+        log.debug('Optical Controller: base_url={:s}'.format(str(base_url)))
+        return base_url
+
+    host = settings.get(VAR_NAME_OPTICAL_CTRL_HOST)
+    port = int(settings.get(VAR_NAME_OPTICAL_CTRL_PORT, 80))
+
+    MSG = 'Optical Controller not found: settings={:s}'
+    if host is None: raise Exception(MSG.format(str(settings)))
+    if port is None: raise Exception(MSG.format(str(settings)))
 
-VAR_NAME_OPTICAL_CONTROLLER_HOST = get_env_var_name(ServiceNameEnum.OPTICALCONTROLLER, ENVVAR_SUFIX_SERVICE_HOST)
-VAR_NAME_OPTICAL_CONTROLLER_PORT = get_env_var_name(ServiceNameEnum.OPTICALCONTROLLER, ENVVAR_SUFIX_SERVICE_PORT_GRPC)
+    schema = settings.get(VAR_NAME_OPTICAL_CTRL_SCHEMA, 'http')
+    base_url = OPTICAL_CTRL_BASE_URL.format(schema, host, port)
+    log.debug('Optical Controller: base_url={:s}'.format(str(base_url)))
+    return base_url
 
-opticalcontrollers_url = find_environment_variables([
-    VAR_NAME_OPTICAL_CONTROLLER_HOST,
-    VAR_NAME_OPTICAL_CONTROLLER_PORT,
-])
-OPTICAL_IP   = opticalcontrollers_url.get(VAR_NAME_OPTICAL_CONTROLLER_HOST)
-OPTICAL_PORT = opticalcontrollers_url.get(VAR_NAME_OPTICAL_CONTROLLER_PORT)
-log.info(str(OPTICAL_IP), str(OPTICAL_PORT))
 
 def get_uuids_from_names(devices: List[Device], device_name: str, port_name: str):
     device_uuid = ""
@@ -79,17 +102,18 @@ def get_device_name_from_uuid(devices: List[Device], device_uuid: str):
 
 
 def add_lightpath(src, dst, bitrate, bidir, ob_band) -> str:
-    if not testing:
+    if not TESTING:
         urlx = ""
         headers = {"Content-Type": "application/json"}
+        base_url = get_optical_controller_base_url()
         if ob_band is None:
             if bidir is None:
                 bidir = 1
-            urlx = "http://{}:{}/OpticalTFS/AddFlexLightpath/{}/{}/{}/{}".format(OPTICAL_IP, OPTICAL_PORT, src, dst, bitrate, bidir)
+            urlx = "{:s}/AddFlexLightpath/{:s}/{:s}/{:s}/{:s}".format(base_url, src, dst, str(bitrate), str(bidir))
         else:
             if bidir is None:
                 bidir = 1
-            urlx = "http://{}:{}/OpticalTFS/AddFlexLightpath/{}/{}/{}/{}/{}".format(OPTICAL_IP, OPTICAL_PORT, src, dst, bitrate, bidir, ob_band)
+            urlx = "{:s}/AddFlexLightpath/{:s}/{:s}/{:s}/{:s}/{:s}".format(base_url, src, dst, str(bitrate), str(bidir), str(ob_band))
         r = requests.put(urlx, headers=headers)
         reply = r.text 
         return reply
@@ -101,8 +125,9 @@ def add_lightpath(src, dst, bitrate, bidir, ob_band) -> str:
                 
 
 def get_optical_band(idx) -> str:
-    if not testing:
-        urlx = "http://{}:{}/OpticalTFS/GetOpticalBand/{}".format(OPTICAL_IP, OPTICAL_PORT, idx)
+    if not TESTING:
+        base_url = get_optical_controller_base_url()
+        urlx = "{:s}/GetOpticalBand/{:s}".format(base_url, str(idx))
         headers = {"Content-Type": "application/json"}
         r = requests.get(urlx, headers=headers)
         reply = r.text 
@@ -116,8 +141,9 @@ def get_optical_band(idx) -> str:
     
 def delete_lightpath(flow_id, src, dst, bitrate) -> str:
     reply = "200"
-    if not testing:
-        urlx = "http://{}:{}/OpticalTFS/DelLightpath/{}/{}/{}/{}".format(OPTICAL_IP, OPTICAL_PORT, flow_id, src, dst, bitrate)
+    if not TESTING:
+        base_url = get_optical_controller_base_url()
+        urlx = "{:s}/DelLightpath/{:s}/{:s}/{:s}/{:s}".format(base_url, str(flow_id), src, dst, str(bitrate))
 
         headers = {"Content-Type": "application/json"}
         r = requests.delete(urlx, headers=headers)
@@ -126,7 +152,8 @@ def delete_lightpath(flow_id, src, dst, bitrate) -> str:
 
 
 def get_lightpaths() -> str:
-    urlx = "http://{}:{}/OpticalTFS/GetLightpaths".format(OPTICAL_IP, OPTICAL_PORT)
+    base_url = get_optical_controller_base_url()
+    urlx = "{:s}/GetLightpaths".format(base_url)
 
     headers = {"Content-Type": "application/json"}
     r = requests.get(urlx, headers=headers)
diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml
index 41b8bb36ca8d3ef7eae444cdc9525cfdf4e48d02..b7345bbd10d16e98e07960f88d15724aa940ed06 100644
--- a/src/tests/.gitlab-ci.yml
+++ b/src/tests/.gitlab-ci.yml
@@ -19,4 +19,4 @@ include:
   - local: '/src/tests/ecoc22/.gitlab-ci.yml'
   #- local: '/src/tests/nfvsdn22/.gitlab-ci.yml'
   #- local: '/src/tests/ofc23/.gitlab-ci.yml'
-  #- local: '/src/tests/ofc24/.gitlab-ci.yml'
+  - local: '/src/tests/ofc24/.gitlab-ci.yml'
diff --git a/src/tests/ecoc22/Dockerfile b/src/tests/ecoc22/Dockerfile
index 3ac134a384a5b61d4dca05fcf9076680c74fe649..28fc91d5ead5cb3020936c7a270fe7856231db86 100644
--- a/src/tests/ecoc22/Dockerfile
+++ b/src/tests/ecoc22/Dockerfile
@@ -72,8 +72,6 @@ COPY src/device/__init__.py device/__init__.py
 COPY src/device/client/. device/client/
 COPY src/monitoring/__init__.py monitoring/__init__.py
 COPY src/monitoring/client/. monitoring/client/
-COPY src/monitoring/__init__.py monitoring/__init__.py
-COPY src/monitoring/client/. monitoring/client/
 COPY src/e2e_orchestrator/__init__.py e2e_orchestrator/__init__.py
 COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/
 COPY src/service/__init__.py service/__init__.py
diff --git a/src/tests/ofc22/.gitlab-ci.yml b/src/tests/ofc22/.gitlab-ci.yml
index 013a389bc438cdac8307f71288f74b0a3afd9737..810e591690b4c9698aad628bef4c63a270c02022 100644
--- a/src/tests/ofc22/.gitlab-ci.yml
+++ b/src/tests/ofc22/.gitlab-ci.yml
@@ -96,7 +96,7 @@ end2end_test ofc22:
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/sliceservice -c server
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/nbiservice -c server
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/monitoringservice -c server
-    - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/ztpservice -c server
+    - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/ztpservice -c ztpservice
     - if docker ps -a | grep ${TEST_NAME}; then docker rm -f ${TEST_NAME}; fi
     - docker images --filter="dangling=true" --quiet | xargs -r docker rmi
   #coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
diff --git a/src/tests/ofc22/Dockerfile b/src/tests/ofc22/Dockerfile
index 4817bd93a313b74851e01bba53174dd14f280a18..4cba83466de077890c39226b58d997230ec844b6 100644
--- a/src/tests/ofc22/Dockerfile
+++ b/src/tests/ofc22/Dockerfile
@@ -72,8 +72,6 @@ COPY src/device/__init__.py device/__init__.py
 COPY src/device/client/. device/client/
 COPY src/monitoring/__init__.py monitoring/__init__.py
 COPY src/monitoring/client/. monitoring/client/
-COPY src/monitoring/__init__.py monitoring/__init__.py
-COPY src/monitoring/client/. monitoring/client/
 COPY src/e2e_orchestrator/__init__.py e2e_orchestrator/__init__.py
 COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/
 COPY src/service/__init__.py service/__init__.py
diff --git a/src/tests/ofc24/.gitlab-ci.yml b/src/tests/ofc24/.gitlab-ci.yml
index 0b5593b160ee14941546242dd3a4c3571407dedf..f169bf7ee08e2faaeff30776080c8128d82e9fe5 100644
--- a/src/tests/ofc24/.gitlab-ci.yml
+++ b/src/tests/ofc24/.gitlab-ci.yml
@@ -13,9 +13,9 @@
 # limitations under the License.
 
 # Build, tag, and push the Docker image to the GitLab Docker registry
-build ofc22:
+build ofc24:
   variables:
-    TEST_NAME: 'ofc22'
+    TEST_NAME: 'ofc24'
   stage: build
   before_script:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
@@ -36,23 +36,65 @@ build ofc22:
       - .gitlab-ci.yml
 
 # Deploy TeraFlowSDN and Execute end-2-end test
-end2end_test ofc22:
+end2end_test ofc24:
   variables:
-    TEST_NAME: 'ofc22'
+    TEST_NAME: 'ofc24'
   stage: end2end_test
   # Disable to force running it after all other tasks
   #needs:
-  #  - build ofc22
+  #  - build ofc24
   before_script:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+    - docker rm -f na-t1 na-t2 na-r1 na-r2
+    - docker network rm -f na-br
+
   script:
     # Download Docker image to run the test
     - docker pull "${CI_REGISTRY_IMAGE}/${TEST_NAME}:latest"
+    - docker pull asgamb1/oc23bgp.img:latest
+    - docker pull asgamb1/flexscale-node.img:latest
 
     # Check MicroK8s is ready
     - microk8s status --wait-ready
     - kubectl get pods --all-namespaces
 
+    # Deploy Optical Device Node Agents
+    - >
+      docker network create -d bridge --subnet=172.254.253.0/24 --gateway=172.254.253.254
+      --ip-range=172.254.253.0/24 na-br
+    - >
+      docker run -dit --init --name na-t1 --network=na-br --ip 172.254.253.101
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh"
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t1.xml:/confd/examples.confd/OC23/platform.xml"
+      asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-t2 --network=na-br --ip 172.254.253.102
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh"
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t2.xml:/confd/examples.confd/OC23/platform.xml"
+      asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-r1 --network=na-br --ip 172.254.253.201
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh"
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r1.xml:/confd/examples.confd/OC23/platform.xml"
+      asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-r2 --network=na-br --ip 172.254.253.202
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh"
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r2.xml:/confd/examples.confd/OC23/platform.xml"
+      asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+
+
+    # Wait for initialization of Optical Device Node Agents
+    - sleep 3
+    - docker ps -a
+    - while ! docker logs na-t1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-t2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-r1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-r2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - sleep 3
+    - docker ps -a
+
+
     # Configure TeraFlowSDN deployment
     # Uncomment if DEBUG log level is needed for the components
     #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/contextservice.yaml
@@ -86,7 +128,9 @@ end2end_test ofc22:
       --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh"
       --volume "$PWD/src/tests/${TEST_NAME}:/opt/results"
       $CI_REGISTRY_IMAGE/${TEST_NAME}:latest
+
   after_script:
+    # Dump TeraFlowSDN component logs
     - source src/tests/${TEST_NAME}/deploy_specs.sh
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/deviceservice -c server
@@ -94,8 +138,23 @@ end2end_test ofc22:
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/serviceservice -c server
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/sliceservice -c server
     - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/nbiservice -c server
+    - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/opticalcontrollerservice -c server
     - if docker ps -a | grep ${TEST_NAME}; then docker rm -f ${TEST_NAME}; fi
+
+    # Dump Optical Device Node Agents container status and logs
+    - docker ps -a
+    - docker logs na-t1
+    - docker logs na-t2
+    - docker logs na-r1
+    - docker logs na-r2
+
+    # Destroy Optical Device Node Agents
+    - docker rm -f na-t1 na-t2 na-r1 na-r2
+    - docker network rm -f na-br
+
+    # Clean old docker images
     - docker images --filter="dangling=true" --quiet | xargs -r docker rmi
+
   #coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
   rules:
     - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)'
diff --git a/src/tests/ofc24/Dockerfile b/src/tests/ofc24/Dockerfile
index 8efa0c72c3bdc72bd336d10a4ffdbc0af025fc25..bef7d25fee87f2539dfb81aaab47d73b4f0fe287 100644
--- a/src/tests/ofc24/Dockerfile
+++ b/src/tests/ofc24/Dockerfile
@@ -70,10 +70,8 @@ COPY src/context/__init__.py context/__init__.py
 COPY src/context/client/. context/client/
 COPY src/device/__init__.py device/__init__.py
 COPY src/device/client/. device/client/
-#COPY src/monitoring/__init__.py monitoring/__init__.py
-#COPY src/monitoring/client/. monitoring/client/
-#COPY src/monitoring/__init__.py monitoring/__init__.py
-#COPY src/monitoring/client/. monitoring/client/
+COPY src/monitoring/__init__.py monitoring/__init__.py
+COPY src/monitoring/client/. monitoring/client/
 COPY src/e2e_orchestrator/__init__.py e2e_orchestrator/__init__.py
 COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/
 COPY src/service/__init__.py service/__init__.py
@@ -82,18 +80,21 @@ COPY src/slice/__init__.py slice/__init__.py
 COPY src/slice/client/. slice/client/
 COPY src/tests/*.py ./tests/
 COPY src/tests/ofc24/__init__.py ./tests/ofc24/__init__.py
-COPY src/tests/ofc24/descriptors_topology.json ./tests/ofc24/descriptors_topology.json
+COPY src/tests/ofc24/descriptors/topology.json ./tests/ofc24/descriptors/topology.json
+COPY src/tests/ofc24/descriptors/service-unidir.json ./tests/ofc24/descriptors/service-unidir.json
+COPY src/tests/ofc24/descriptors/service-bidir.json ./tests/ofc24/descriptors/service-bidir.json
 COPY src/tests/ofc24/tests/. ./tests/ofc24/tests/
-COPY src/tests/tools/. ./tests/tools/
 
 RUN tee ./run_tests.sh <<EOF
 #!/bin/bash
 source /var/teraflow/tfs_runtime_env_vars.sh
 export PYTHONPATH=/var/teraflow
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_bootstrap.py      --junitxml=/opt/results/report_bootstrap.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service.py --junitxml=/opt/results/report_create_service.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service.py --junitxml=/opt/results/report_delete_service.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_cleanup.py        --junitxml=/opt/results/report_cleanup.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_bootstrap.py             --junitxml=/opt/results/report_bootstrap.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service_unidir.py --junitxml=/opt/results/report_create_service_unidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service_unidir.py --junitxml=/opt/results/report_delete_service_unidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service_bidir.py  --junitxml=/opt/results/report_create_service_bidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service_bidir.py  --junitxml=/opt/results/report_delete_service_bidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_cleanup.py               --junitxml=/opt/results/report_cleanup.xml
 EOF
 RUN chmod ug+x ./run_tests.sh
 
diff --git a/src/tests/ofc24/__init__.py b/src/tests/ofc24/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..1549d9811aa5d1c193a44ad45d0d7773236c0612
--- /dev/null
+++ b/src/tests/ofc24/__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.
+
diff --git a/src/tests/ofc24/README.md b/src/tests/ofc24/_old/README.md
similarity index 100%
rename from src/tests/ofc24/README.md
rename to src/tests/ofc24/_old/README.md
diff --git a/src/tests/ofc24/_old/startExtraNetConfigAgent.sh b/src/tests/ofc24/_old/startExtraNetConfigAgent.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f8638a51f289af6718e388db583d94da40653efb
--- /dev/null
+++ b/src/tests/ofc24/_old/startExtraNetConfigAgent.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# 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.
+
+         
+
+
+
+screen -dmS t1 -T xterm sh -c "docker run -p 10.0.2.4:2023:2022 -v ~/tfs-ctrl/src/tests/ofc24/tempOC/files:/files --name na1 -it asgamb1/oc23bgp.img:latest bash"
+screen -dmS t2 -T xterm sh -c "docker run -p 10.0.2.4:2024:2022 -v ~/tfs-ctrl/src/tests/ofc24/tempOC/files:/files --name na2 -it asgamb1/oc23bgp.img:latest bash"
+
+
+
+sleep 4
+echo "starting transponder1 "
+
+if [ "$( docker container  inspect -f '{{.State.Running}}' na1)" = "true" ]; then 
+        docker exec  na1 sh -c  " cp /files/platform_t1.xml demoECOC21.xml ; 
+                                 /confd/examples.confd/OC23/startNetconfAgent.sh;"
+       
+else 
+        echo "na1 container is not running yet"
+fi
+
+echo "starting transponder2 "
+
+if [ "$( docker container  inspect -f '{{.State.Running}}' na2)" = "true" ]; then 
+        docker exec  na2 sh -c " cp /files/platform_t2.xml demoECOC21.xml;
+                                  /confd/examples.confd/OC23/startNetconfAgent.sh  "
+
+else 
+        echo "na2 container is not running yet"
+fi
\ No newline at end of file
diff --git a/src/tests/ofc24/_old/start_topo.sh b/src/tests/ofc24/_old/start_topo.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ed93641c99420768271279735c8430c5a462935d
--- /dev/null
+++ b/src/tests/ofc24/_old/start_topo.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+# 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.
+
+IMAGE_NAME="asgamb1/oc23bgp.img:latest"
+DOCKER_CONTAINER=$1
+DOCKER_PORT="2022"
+
+sudo docker stop na1 -t 1
+sudo docker stop na2 -t 1
+sudo docker stop na3 -t 1
+sudo docker stop na4 -t 1
+
+sudo docker rm na2
+sudo docker rm na1
+sudo docker rm na3
+sudo docker rm na4
+
+echo "Creating Transponder Agents"
+
+ ./startExtraNetConfigAgent.sh
+
+
+echo "Creating Roadms Agents"
+
+
+screen -dmS t3 -T xterm sh -c 'docker run -p 10.0.2.4:2025:2022 -v  ~/tfs-ctrl/src/tests/ofc24/tempOC/files:/files --name na3 -it asgamb1/flexscale-node.img:latest bash '
+screen -dmS t4 -T xterm sh -c 'docker run -p 10.0.2.4:2026:2022 -v  ~/tfs-ctrl/src/tests/ofc24/tempOC/files:/files --name na4 -it asgamb1/flexscale-node.img:latest bash '
+sleep 4
+
+echo "starting Roadm1 "
+
+if [ "$( docker container  inspect -f '{{.State.Running}}' na4)" = "true" ]; then 
+        docker exec  na4   sh -c " cp /files/platform_r2.xml init_openconfig-platform.xml; 
+                                  cp /files/startNetconfAgent.sh startNetconfAgent.sh;
+                                  /confd/examples.confd/OC23/startNetconfAgent.sh ;"&
+      
+else 
+        echo "na4  is not running yet"
+fi
+
+
+echo "starting Roadm2 "
+
+
+
+if [ "$( docker container  inspect -f '{{.State.Running}}' na3)" = "true" ]; then 
+        docker exec  na3 sh -c " cp /files/platform_r1.xml init_openconfig-platform.xml;
+                                 cp /files/startNetconfAgent.sh startNetconfAgent.sh;
+                                 /confd/examples.confd/OC23/startNetconfAgent.sh; " 
+
+else 
+        echo "na3  is not running yet"
+fi
+
+
+# screen -S t3 -X stuff "cp ~/files/platform_r1.xml /confd/examples.confd/OC23/init_openconfig-platform.xml && ./startNetconfAgent.sh"
+# bash -c "docker  cp  ~/tfs-ctrl/src/tests/ofc24/tempOC/files/platform_r2.xml na4:/confd/examples.confd/OC23/init_openconfig-platform.xml;
+#  docker  cp   ~/tfs-ctrl/src/tests/ofc24/tempOC/files/startNetconfAgent.sh na4:/confd/examples.confd/OC23/startNetconfAgent.sh;"
diff --git a/src/tests/ofc24/deploy-node-agents.sh b/src/tests/ofc24/deploy-node-agents.sh
index 5c3c8d0d2a5c4e15f4d3dda6a00d90ff00b77539..1c1e455268ecb542eecc8f4292e931575145f415 100755
--- a/src/tests/ofc24/deploy-node-agents.sh
+++ b/src/tests/ofc24/deploy-node-agents.sh
@@ -17,8 +17,8 @@ TEST_NAME="ofc24"
 
 
 echo
-echo "Pre-deploy clean-up:"
-echo "--------------------"
+echo "Clean-up:"
+echo "---------"
 docker rm -f na-t1 na-t2 na-r1 na-r2
 docker network rm na-br
 
@@ -26,28 +26,29 @@ docker network rm na-br
 echo
 echo "Pull Docker images:"
 echo "-------------------"
-docker pull asgamb1/flexscale-hhi.img:latest
+docker pull asgamb1/oc23bgp.img:latest
 docker pull asgamb1/flexscale-node.img:latest
 
+
 echo
 echo "Create Management Network and Node Agents:"
 echo "------------------------------------------"
 docker network create -d bridge --subnet=172.254.253.0/24 --gateway=172.254.253.254 --ip-range=172.254.253.0/24 na-br
-docker run -d --name na-t1 --network=na-br --ip 172.254.253.1 \
-    --volume "$PWD/src/tests/${TEST_NAME}/startNetconfAgent.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
-    --volume "$PWD/src/tests/${TEST_NAME}/platform_t1.xml:/confd/examples.confd/OC23/init_openconfig-platform.xml" \
-    asgamb1/flexscale-hhi.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
-docker run -d --name na-t2 --network=na-br --ip 172.254.253.2 \
-    --volume "$PWD/src/tests/${TEST_NAME}/startNetconfAgent.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
-    --volume "$PWD/src/tests/${TEST_NAME}/platform_t2.xml:/confd/examples.confd/OC23/init_openconfig-platform.xml" \
-    asgamb1/flexscale-hhi.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
-docker run -d --name na-r1 --network=na-br --ip 172.254.253.101 \
-    --volume "$PWD/src/tests/${TEST_NAME}/startNetconfAgent.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
-    --volume "$PWD/src/tests/${TEST_NAME}/platform_r1.xml:/confd/examples.confd/OC23/init_openconfig-platform.xml" \
+docker run -dit --init --name na-t1 --network=na-br --ip 172.254.253.101 \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t1.xml:/confd/examples.confd/OC23/platform.xml" \
+    asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+docker run -dit --init --name na-t2 --network=na-br --ip 172.254.253.102 \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t2.xml:/confd/examples.confd/OC23/platform.xml" \
+    asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+docker run -dit --init --name na-r1 --network=na-br --ip 172.254.253.201 \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r1.xml:/confd/examples.confd/OC23/platform.xml" \
     asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
-docker run -d --name na-r2 --network=na-br --ip 172.254.253.102 \
-    --volume "$PWD/src/tests/${TEST_NAME}/startNetconfAgent.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
-    --volume "$PWD/src/tests/${TEST_NAME}/platform_r2.xml:/confd/examples.confd/OC23/init_openconfig-platform.xml" \
+docker run -dit --init --name na-r2 --network=na-br --ip 172.254.253.202 \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+    --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r2.xml:/confd/examples.confd/OC23/platform.xml" \
     asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
 
 
@@ -55,13 +56,11 @@ echo
 echo "Waiting for initialization..."
 echo "-----------------------------"
 docker ps -a
-sleep 5
-docker ps -a
 while ! docker logs na-t1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
 while ! docker logs na-t2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
 while ! docker logs na-r1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
 while ! docker logs na-r2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
-sleep 2
+sleep 3
 docker ps -a
 
 
@@ -69,16 +68,10 @@ echo
 echo "Dump Node Agent status:"
 echo "-----------------------"
 docker ps -a
-#docker logs na-t1
-#docker logs na-t2
-#docker logs na-r1
-#docker logs na-r2
-
+docker logs na-t1
+docker logs na-t2
+docker logs na-r1
+docker logs na-r2
 
-#echo
-#echo "Post-test clean-up:"
-#echo "-------------------"
-#docker rm -f na-t1 na-t2 na-r1 na-r2
-#docker network rm na-br
 
 echo "Done!"
diff --git a/src/tests/ofc24/deploy_specs.sh b/src/tests/ofc24/deploy_specs.sh
index ca5494de25ea17c08c2df1f2d62923c59b0e81e2..4ade7592363981bb8d51612fa9339b93b53dd4c3 100755
--- a/src/tests/ofc24/deploy_specs.sh
+++ b/src/tests/ofc24/deploy_specs.sh
@@ -30,7 +30,14 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui"
 #export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker"
 
 # Uncomment to activate Optical Controller
-export TFS_COMPONENTS="${TFS_COMPONENTS} opticalcontroller"
+# To manage optical connections, "service" requires "opticalcontroller" to be deployed
+# before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+# "opticalcontroller" only if "service" is already in TFS_COMPONENTS, and re-export it.
+if [[ "$TFS_COMPONENTS" == *"service"* ]]; then
+    BEFORE="${TFS_COMPONENTS% service*}"
+    AFTER="${TFS_COMPONENTS#* service}"
+    export TFS_COMPONENTS="${BEFORE} opticalcontroller service ${AFTER}"
+fi
 
 # Uncomment to activate ZTP
 #export TFS_COMPONENTS="${TFS_COMPONENTS} ztp"
@@ -63,7 +70,7 @@ export TFS_K8S_NAMESPACE="tfs"
 export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml"
 
 # Uncomment to monitor performance of components
-export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/servicemonitors.yaml"
+#export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/servicemonitors.yaml"
 
 # Uncomment when deploying Optical CyberSecurity
 #export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/cachingservice.yaml"
diff --git a/src/tests/ofc24/1.context.json b/src/tests/ofc24/descriptors/old/1.context.json
similarity index 100%
rename from src/tests/ofc24/1.context.json
rename to src/tests/ofc24/descriptors/old/1.context.json
diff --git a/src/tests/ofc24/2.device1.json b/src/tests/ofc24/descriptors/old/2.device1.json
similarity index 95%
rename from src/tests/ofc24/2.device1.json
rename to src/tests/ofc24/descriptors/old/2.device1.json
index 3e31f31eb84630415f96b6af1e6ae5d34bdb1c89..c5a189e3165f4d170fb1c9e8b13314a202a84630 100755
--- a/src/tests/ofc24/2.device1.json
+++ b/src/tests/ofc24/descriptors/old/2.device1.json
@@ -41,7 +41,7 @@
                         "action": 1,
                         "custom": {
                             "resource_key": "_connect/address",
-                            "resource_value": "10.0.2.15"
+                            "resource_value": "10.0.2.4"
                         }
                     },
                     {
diff --git a/src/tests/ofc24/3.device2.json b/src/tests/ofc24/descriptors/old/3.device2.json
similarity index 95%
rename from src/tests/ofc24/3.device2.json
rename to src/tests/ofc24/descriptors/old/3.device2.json
index 812affa7b8540b67f83d6f3c9bb9b5442c44fd0d..a38fc290508788637a1ebc1cef7ddc54514a46fe 100755
--- a/src/tests/ofc24/3.device2.json
+++ b/src/tests/ofc24/descriptors/old/3.device2.json
@@ -41,7 +41,7 @@
                         "action": 1,
                         "custom": {
                             "resource_key": "_connect/address",
-                            "resource_value": "10.0.2.15"
+                            "resource_value": "10.0.2.4"
                         }
                     },
                     {
diff --git a/src/tests/ofc24/4.device3_R1.json b/src/tests/ofc24/descriptors/old/4.device3_R1.json
similarity index 96%
rename from src/tests/ofc24/4.device3_R1.json
rename to src/tests/ofc24/descriptors/old/4.device3_R1.json
index 3a57ba79cd2ff8aa6d4b666ac382932ade2f20e0..1c110f20ecb6c2d5081044c961dbf50e3d4c816d 100755
--- a/src/tests/ofc24/4.device3_R1.json
+++ b/src/tests/ofc24/descriptors/old/4.device3_R1.json
@@ -107,7 +107,7 @@
                         "action": 1,
                         "custom": {
                             "resource_key": "_connect/address",
-                            "resource_value": "10.0.2.15"
+                            "resource_value": "10.0.2.4"
                         }
                     },
                     {
diff --git a/src/tests/ofc24/5.device4_R2.json b/src/tests/ofc24/descriptors/old/5.device4_R2.json
similarity index 96%
rename from src/tests/ofc24/5.device4_R2.json
rename to src/tests/ofc24/descriptors/old/5.device4_R2.json
index 9b1968d095c3e2c28c058b22f7295d7d1cbda380..43ebda5c6c139b4f2d8b2a51e7f42a257a38b4d4 100755
--- a/src/tests/ofc24/5.device4_R2.json
+++ b/src/tests/ofc24/descriptors/old/5.device4_R2.json
@@ -108,7 +108,7 @@
                         "action": 1,
                         "custom": {
                             "resource_key": "_connect/address",
-                            "resource_value": "10.0.2.15"
+                            "resource_value": "10.0.2.4"
                         }
                     },
                     {
diff --git a/src/tests/ofc24/6.links.json b/src/tests/ofc24/descriptors/old/6.links.json
similarity index 100%
rename from src/tests/ofc24/6.links.json
rename to src/tests/ofc24/descriptors/old/6.links.json
diff --git a/src/tests/ofc24/7.service-bidir.json b/src/tests/ofc24/descriptors/service-bidir.json
similarity index 100%
rename from src/tests/ofc24/7.service-bidir.json
rename to src/tests/ofc24/descriptors/service-bidir.json
diff --git a/src/tests/ofc24/7.service-unidir.json b/src/tests/ofc24/descriptors/service-unidir.json
similarity index 100%
rename from src/tests/ofc24/7.service-unidir.json
rename to src/tests/ofc24/descriptors/service-unidir.json
diff --git a/src/tests/ofc24/descriptors_topology.json b/src/tests/ofc24/descriptors/topology.json
similarity index 96%
rename from src/tests/ofc24/descriptors_topology.json
rename to src/tests/ofc24/descriptors/topology.json
index 6ae521c7642b24d428b26b11c29882ca46995014..85bbad55eb8608d2d2a7abad7fffab8fffdae682 100644
--- a/src/tests/ofc24/descriptors_topology.json
+++ b/src/tests/ofc24/descriptors/topology.json
@@ -16,8 +16,8 @@
                 }}
             ],
             "device_config": {"config_rules": [
-                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.15"}},
-                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2023"}},
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.254.253.101"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2022"}},
                 {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {
                     "username": "admin", "password": "admin", "force_running": false, "hostkey_verify": false,
                     "look_for_keys": false, "allow_agent": false, "commit_per_rule": false,
@@ -36,8 +36,8 @@
                 }}
             ],
             "device_config": {"config_rules": [
-                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.15"}},
-                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2024"}},
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.254.253.102"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2022"}},
                 {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {
                     "username": "admin", "password": "admin", "force_running": false, "hostkey_verify": false,
                     "look_for_keys": false, "allow_agent": false, "commit_per_rule": false,
@@ -68,8 +68,8 @@
                 }}
             ],
             "device_config": {"config_rules": [
-                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.15"}},
-                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2025"}},
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.254.253.201"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2022"}},
                 {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {
                     "username": "admin", "password": "admin", "force_running": false, "hostkey_verify": false,
                     "look_for_keys": false, "allow_agent": false, "commit_per_rule": false,
@@ -105,8 +105,8 @@
                 }}
             ],
             "device_config": {"config_rules": [
-                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.15"}},
-                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2026"}},
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.254.253.202"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "2022"}},
                 {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {
                     "username": "admin", "password": "admin", "force_running": false, "hostkey_verify": false,
                     "look_for_keys": false, "allow_agent": false, "commit_per_rule": false,
diff --git a/src/tests/ofc24/destroy-node-agents.sh b/src/tests/ofc24/destroy-node-agents.sh
new file mode 100755
index 0000000000000000000000000000000000000000..19e7fc9a9398734aa967cfb5618d125c545500a2
--- /dev/null
+++ b/src/tests/ofc24/destroy-node-agents.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# 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.
+
+echo
+echo "Clean-up:"
+echo "---------"
+docker rm -f na-t1 na-t2 na-r1 na-r2
+docker network rm na-br
+
+echo "Done!"
diff --git a/src/tests/ofc24/platform_r1.xml b/src/tests/ofc24/node-agents-config/platform_r1.xml
old mode 100755
new mode 100644
similarity index 100%
rename from src/tests/ofc24/platform_r1.xml
rename to src/tests/ofc24/node-agents-config/platform_r1.xml
diff --git a/src/tests/ofc24/platform_r2.xml b/src/tests/ofc24/node-agents-config/platform_r2.xml
old mode 100755
new mode 100644
similarity index 100%
rename from src/tests/ofc24/platform_r2.xml
rename to src/tests/ofc24/node-agents-config/platform_r2.xml
diff --git a/src/tests/ofc24/platform_t1.xml b/src/tests/ofc24/node-agents-config/platform_t1.xml
old mode 100755
new mode 100644
similarity index 100%
rename from src/tests/ofc24/platform_t1.xml
rename to src/tests/ofc24/node-agents-config/platform_t1.xml
diff --git a/src/tests/ofc24/platform_t2.xml b/src/tests/ofc24/node-agents-config/platform_t2.xml
old mode 100755
new mode 100644
similarity index 100%
rename from src/tests/ofc24/platform_t2.xml
rename to src/tests/ofc24/node-agents-config/platform_t2.xml
diff --git a/src/tests/ofc24/node-agents-config/startNetconfAgent-mg-on.sh b/src/tests/ofc24/node-agents-config/startNetconfAgent-mg-on.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e54496b408f055853aa396e28aa040a2052293fa
--- /dev/null
+++ b/src/tests/ofc24/node-agents-config/startNetconfAgent-mg-on.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+# 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.
+
+echo 'Cleaning...'
+make clean
+
+echo 'Rebuilding...'
+make all
+
+echo 'Initializing database...'
+cp platform.xml confd-cdb/
+
+echo 'Starting ConfD...'
+make start2
+
+echo 'ConfD Ready!!'
diff --git a/src/tests/ofc24/node-agents-config/startNetconfAgent-tp.sh b/src/tests/ofc24/node-agents-config/startNetconfAgent-tp.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4e2ec068662b13c82248575478b4c88832d45e5f
--- /dev/null
+++ b/src/tests/ofc24/node-agents-config/startNetconfAgent-tp.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# 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.
+
+echo 'Cleaning...'
+make clean
+
+echo 'Rebuilding...'
+make all
+
+echo 'Initializing database...'
+cp platform.xml confd-cdb/
+cp interfaces.xml confd-cdb/
+cp bgp.xml confd-cdb/
+
+echo 'Starting ConfD...'
+make start2
+
+echo 'ConfD Ready!!'
diff --git a/src/tests/ofc24/run-tests-locally.sh b/src/tests/ofc24/run-tests-locally.sh
new file mode 100755
index 0000000000000000000000000000000000000000..14cf78500d7d61789129b89cdfffe0f28e793aa5
--- /dev/null
+++ b/src/tests/ofc24/run-tests-locally.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# 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.
+
+source ~/tfs-ctrl/tfs_runtime_env_vars.sh
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_bootstrap.py
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_create_service_unidir.py
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_delete_service_unidir.py
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_create_service_bidir.py
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_delete_service_bidir.py
+pytest --verbose --log-level=INFO ~/tfs-ctrl/ofc24/tests/test_functional_cleanup.py
diff --git a/src/tests/ofc24/startExtraNetConfigAgent.sh b/src/tests/ofc24/startExtraNetConfigAgent.sh
deleted file mode 100755
index d9428585ef1040bb0440bf6db100b7f2bc71c970..0000000000000000000000000000000000000000
--- a/src/tests/ofc24/startExtraNetConfigAgent.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-# 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.
-
-DOCKER_CONTAINER=$1
-DOCKER_PORT=$2
-
-if [ -n "$DOCKER_CONTAINER" ] && [ -n "$DOCKER_PORT" ];then
-      sudo docker stop "$DOCKER_CONTAINER" -t 1
-      sudo docker rm "$DOCKER_CONTAINER"
-
-      echo "Creating TPs"
-      screen -dmS t1 -T xterm sh -c "docker run -p 10.0.2.15:"$DOCKER_PORT":2022 -v ~/tfs-ctrl/tempOC/files:/files --name $DOCKER_CONTAINER -it asgamb1/oc23bgp.img:latest"
-      sleep  2 
-      if [ "$( docker container  inspect -f '{{.State.Running}}' "$DOCKER_CONTAINER")" = "true" ]; then 
-            docker exec  "$DOCKER_CONTAINER"  cp /files/demoECOC21_4.xml demoECOC21.xml
-            docker exec "$DOCKER_CONTAINER" /confd/examples.confd/OC23/startNetconfAgent.sh 
-      else 
-            echo "your container is not running yet"
-      fi
-else 
-   echo "Please define the docker container name and port"
-fi         
diff --git a/src/tests/ofc24/startNetconfAgent.sh b/src/tests/ofc24/startNetconfAgent.sh
deleted file mode 100755
index 10b721883799c4fd257e1f627ff1480259037702..0000000000000000000000000000000000000000
--- a/src/tests/ofc24/startNetconfAgent.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-make clean
-make all
-#make init
-cp init_openconfig-platform.xml confd-cdb/
-#cp init_flex-scale-mg-on.xml confd-cdb/
-make start2
diff --git a/src/tests/ofc24/start_topo.sh b/src/tests/ofc24/start_topo.sh
deleted file mode 100755
index c924064763c14e4da45344cd21f4d9c81c9640a9..0000000000000000000000000000000000000000
--- a/src/tests/ofc24/start_topo.sh
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/bin/bash
-# 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.
-
-IMAGE_NAME="asgamb1/oc23bgp.img:latest"
-DOCKER_CONTAINER=$1
-DOCKER_PORT="2022"
-
-sudo docker stop na1 -t 1
-sudo docker stop na2 -t 1
-sudo docker stop na3 -t 1
-sudo docker stop na4 -t 1
-
-sudo docker rm na2
-sudo docker rm na1
-sudo docker rm na3
-sudo docker rm na4
-
-echo "Creating Transponder Agents"
-
-# if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1 ; then
-#    echo "asgamb1/oc23bgp.img:latest not existed ! "
-#    screen -dmS t3 -T xterm sh -c "docker run -p 10.0.2.15:2025:2022 -v ~/tempOC/files:/files --name na -it  $IMAGE_NAME bash"
-#    echo 'start downloading  asgamb1/oc23bgp.img:latest , it may take few minutes ! .... ' 
-#    while [ "$(docker image inspect asgamb1/oc23bgp.img:latest 2>/dev/null)" == "[]" ]; do
-#         sleep 1
-# done
-
-#fi
-
-
-
-screen -dmS t1 -T xterm sh -c "docker run  -p 127.0.0.1:2023:2022 -v ~/tempOC/files:/files --name $DOCKER_CONTAINER -it asgamb1/oc23bgp.img:latest bash"
-sleep  2 
-if [ "$( docker container  inspect -f '{{.State.Running}}' "$DOCKER_CONTAINER")" = "true" ]; then 
-        docker exec  "$DOCKER_CONTAINER"  cp /files/demoECOC21_4.xml demoECOC21.xml
-        docker exec "$DOCKER_CONTAINER" /confd/examples.confd/OC23/startNetconfAgent.sh 
-else 
-        echo "your container is not running yet"
-fi
-
-echo " It may take a while , Hang on ..."
-source "./startExtraNetConfigAgent.sh"  "na1" "2023"
-sleep 3
-
-source "./startExtraNetConfigAgent.sh"  "na2" "2024"
-sleep 3
-
-bash -c "cp /tempOC/files/plat_r1.xml /confd/examples.confd/OC23/init_openconfig-platform.xml; ./startNetconfAgent.sh"
-bash -c "cp /tempOC/files/plat_r2.xml /confd/examples.confd/OC23/init_openconfig-platform.xml; ./startNetconfAgent.sh"
-screen -dmS t3 -T xterm sh -c 'docker run -p 10.0.2.15:2025:2022 -v ~/tfs-ctrl/tempOC/files:/files --name na3 -it asgamb1/flexscale-node.img:latest ./startNetconfAgent.sh'
-screen -dmS t4 -T xterm sh -c 'docker run -p 10.0.2.15:2026:2022 -v ~/tfs-ctrl/tempOC/files:/files --name na4 -it asgamb1/flexscale-node.img:latest ./startNetconfAgent.sh'
diff --git a/src/tests/ofc24/tests/test_functional_bootstrap.py b/src/tests/ofc24/tests/test_functional_bootstrap.py
index bc648d16de57c8c287c7d601f994cb81ed45bc04..e6562f8a2f95f3d1f504b2bbc5dd6dc3b4c47077 100644
--- a/src/tests/ofc24/tests/test_functional_bootstrap.py
+++ b/src/tests/ofc24/tests/test_functional_bootstrap.py
@@ -24,7 +24,7 @@ from tests.Fixtures import context_client, device_client # pylint: disable=unuse
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_topology.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'topology.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
 def test_scenario_bootstrap(
diff --git a/src/tests/ofc24/tests/test_functional_cleanup.py b/src/tests/ofc24/tests/test_functional_cleanup.py
index 5f1ce23f13051759e0e688a42c7118eaff8d3c72..281b0969d708d07be8bdda0aca83b8976d6fa1dd 100644
--- a/src/tests/ofc24/tests/test_functional_cleanup.py
+++ b/src/tests/ofc24/tests/test_functional_cleanup.py
@@ -24,7 +24,7 @@ from tests.Fixtures import context_client, device_client    # pylint: disable=un
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_topology.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'topology.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
 def test_scenario_cleanup(
diff --git a/src/tests/ofc24/tests/test_functional_create_service.py b/src/tests/ofc24/tests/test_functional_create_service.py
deleted file mode 100644
index 74c74483eb82325afec2cae833ebd460566da153..0000000000000000000000000000000000000000
--- a/src/tests/ofc24/tests/test_functional_create_service.py
+++ /dev/null
@@ -1,102 +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.
-
-import logging, os, random
-from common.Constants import DEFAULT_CONTEXT_NAME
-from common.proto.context_pb2 import ContextId, Empty, ServiceTypeEnum
-from common.proto.kpi_sample_types_pb2 import KpiSampleType
-from common.tools.descriptor.Loader import DescriptorLoader
-from common.tools.grpc.Tools import grpc_message_to_json_string
-from common.tools.object_factory.Context import json_context_id
-from context.client.ContextClient import ContextClient
-from monitoring.client.MonitoringClient import MonitoringClient
-from tests.Fixtures import context_client, monitoring_client                    # pylint: disable=unused-import
-from tests.tools.mock_osm.MockOSM import MockOSM
-from .Fixtures import osm_wim                                                   # pylint: disable=unused-import
-from .Objects import WIM_SERVICE_CONNECTION_POINTS, WIM_SERVICE_TYPE
-
-LOGGER = logging.getLogger(__name__)
-LOGGER.setLevel(logging.DEBUG)
-
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_emulated.json')
-ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
-
-def test_service_creation(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
-    # Load descriptors and validate the base scenario
-    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
-    descriptor_loader.validate()
-
-    # Verify the scenario has no services/slices
-    response = context_client.GetContext(ADMIN_CONTEXT_ID)
-    assert len(response.service_ids) == 0
-    assert len(response.slice_ids) == 0
-
-    # Create Connectivity Service
-    service_uuid = osm_wim.create_connectivity_service(WIM_SERVICE_TYPE, WIM_SERVICE_CONNECTION_POINTS)
-    osm_wim.get_connectivity_service_status(service_uuid)
-
-    # Ensure slices and services are created
-    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
-    LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
-    assert len(response.slices) == 1 # OSM slice
-
-    response = context_client.ListServices(ADMIN_CONTEXT_ID)
-    LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
-    assert len(response.services) == 2 # 1xL3NM + 1xTAPI
-
-    for service in response.services:
-        service_id = service.service_id
-        response = context_client.ListConnections(service_id)
-        LOGGER.info('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
-            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
-
-        if service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM:
-            assert len(response.connections) == 1 # 1 connection per service
-        elif service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
-            assert len(response.connections) == 1 # 1 connection per service
-        else:
-            str_service = grpc_message_to_json_string(service)
-            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
-
-
-def test_scenario_kpi_values_created(
-    monitoring_client: MonitoringClient,    # pylint: disable=redefined-outer-name
-) -> None:
-    """
-    This test validates that KPI values have been inserted into the monitoring database.
-    We short k KPI descriptors to test.
-    """
-    response = monitoring_client.GetKpiDescriptorList(Empty())
-    kpi_descriptors = random.choices(response.kpi_descriptor_list, k=2)
-
-    for kpi_descriptor in kpi_descriptors:
-        MSG = 'KPI(kpi_uuid={:s}, device_uuid={:s}, endpoint_uuid={:s}, service_uuid={:s}, kpi_sample_type={:s})...'
-        LOGGER.info(MSG.format(
-            str(kpi_descriptor.kpi_id.kpi_id.uuid), str(kpi_descriptor.device_id.device_uuid.uuid),
-            str(kpi_descriptor.endpoint_id.endpoint_uuid.uuid), str(kpi_descriptor.service_id.service_uuid.uuid),
-            str(KpiSampleType.Name(kpi_descriptor.kpi_sample_type))))
-        response = monitoring_client.GetInstantKpi(kpi_descriptor.kpi_id)
-        kpi_uuid = response.kpi_id.kpi_id.uuid
-        assert kpi_uuid == kpi_descriptor.kpi_id.kpi_id.uuid
-        kpi_value_type = response.kpi_value.WhichOneof('value')
-        if kpi_value_type is None:
-            MSG = '  KPI({:s}): No instant value found'
-            LOGGER.warning(MSG.format(str(kpi_uuid)))
-        else:
-            kpi_timestamp = response.timestamp.timestamp
-            assert kpi_timestamp > 0
-            assert kpi_value_type == 'floatVal'
-            kpi_value = getattr(response.kpi_value, kpi_value_type)
-            MSG = '  KPI({:s}): timestamp={:s} value_type={:s} value={:s}'
-            LOGGER.info(MSG.format(str(kpi_uuid), str(kpi_timestamp), str(kpi_value_type), str(kpi_value)))
diff --git a/src/tests/ofc24/tests/test_functional_create_service_bidir.py b/src/tests/ofc24/tests/test_functional_create_service_bidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..e910c946d509a814415019c01a19b1058934e47a
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_create_service_bidir.py
@@ -0,0 +1,72 @@
+# 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 logging, os
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceStatusEnum, ServiceTypeEnum
+from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from context.client.ContextClient import ContextClient
+from device.client.DeviceClient import DeviceClient
+from service.client.ServiceClient import ServiceClient
+from tests.Fixtures import context_client, device_client, service_client        # pylint: disable=unused-import
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-bidir.json')
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_creation_bidir(
+    context_client : ContextClient, # pylint: disable=redefined-outer-name
+    device_client  : DeviceClient,  # pylint: disable=redefined-outer-name
+    service_client : ServiceClient, # pylint: disable=redefined-outer-name
+):
+    # Load descriptors and validate the base scenario
+    descriptor_loader = DescriptorLoader(
+        descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client,
+        service_client=service_client
+    )
+    results = descriptor_loader.process()
+    check_descriptor_load_results(results, descriptor_loader)
+
+    # Verify the scenario has 1 service and 0 slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 1
+    assert len(response.slice_ids) == 0
+
+    # Check there are no slices
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 0
+
+    # Check there is 1 service
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 1
+
+    for service in response.services:
+        service_id = service.service_id
+        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE
+
+        response = context_client.ListConnections(service_id)
+        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
+            assert len(response.connections) == 2
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
diff --git a/src/tests/ofc24/tests/test_functional_create_service_unidir.py b/src/tests/ofc24/tests/test_functional_create_service_unidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b2550ae1e6f7ea9d51aec70af7af7b5c1360dc4
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_create_service_unidir.py
@@ -0,0 +1,72 @@
+# 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 logging, os
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceStatusEnum, ServiceTypeEnum
+from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from context.client.ContextClient import ContextClient
+from device.client.DeviceClient import DeviceClient
+from service.client.ServiceClient import ServiceClient
+from tests.Fixtures import context_client, device_client, service_client        # pylint: disable=unused-import
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-bidir.json')
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_creation_unidir(
+    context_client : ContextClient, # pylint: disable=redefined-outer-name
+    device_client  : DeviceClient,  # pylint: disable=redefined-outer-name
+    service_client : ServiceClient, # pylint: disable=redefined-outer-name
+):
+    # Load descriptors and validate the base scenario
+    descriptor_loader = DescriptorLoader(
+        descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client,
+        service_client=service_client
+    )
+    results = descriptor_loader.process()
+    check_descriptor_load_results(results, descriptor_loader)
+
+    # Verify the scenario has 1 service and 0 slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 1
+    assert len(response.slice_ids) == 0
+
+    # Check there are no slices
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 0
+
+    # Check there is 1 service
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 1
+
+    for service in response.services:
+        service_id = service.service_id
+        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE
+
+        response = context_client.ListConnections(service_id)
+        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
+            assert len(response.connections) == 2
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
diff --git a/src/tests/ofc24/tests/test_functional_delete_service.py b/src/tests/ofc24/tests/test_functional_delete_service.py
deleted file mode 100644
index daff29064f07e8117a4503fc243c7acac9f88bc6..0000000000000000000000000000000000000000
--- a/src/tests/ofc24/tests/test_functional_delete_service.py
+++ /dev/null
@@ -1,74 +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.
-
-import logging, os
-from common.Constants import DEFAULT_CONTEXT_NAME
-from common.proto.context_pb2 import ContextId, ServiceTypeEnum
-from common.tools.descriptor.Loader import DescriptorLoader
-from common.tools.grpc.Tools import grpc_message_to_json_string
-from common.tools.object_factory.Context import json_context_id
-from context.client.ContextClient import ContextClient
-from tests.Fixtures import context_client   # pylint: disable=unused-import
-from tests.tools.mock_osm.MockOSM import MockOSM
-from .Fixtures import osm_wim               # pylint: disable=unused-import
-
-LOGGER = logging.getLogger(__name__)
-LOGGER.setLevel(logging.DEBUG)
-
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_emulated.json')
-ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
-
-def test_service_removal(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
-    # Ensure slices and services are created
-    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
-    LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
-    assert len(response.slices) == 1 # OSM slice
-
-    response = context_client.ListServices(ADMIN_CONTEXT_ID)
-    LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
-    assert len(response.services) == 2 # 1xL3NM + 1xTAPI
-
-    service_uuids = set()
-    for service in response.services:
-        service_id = service.service_id
-        response = context_client.ListConnections(service_id)
-        LOGGER.info('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
-            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
-
-        if service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM:
-            assert len(response.connections) == 1 # 1 connection per service
-            service_uuid = service_id.service_uuid.uuid
-            service_uuids.add(service_uuid)
-            osm_wim.conn_info[service_uuid] = {}
-        elif service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
-            assert len(response.connections) == 1 # 1 connection per service
-        else:
-            str_service = grpc_message_to_json_string(service)
-            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
-
-    # Identify service to delete
-    assert len(service_uuids) == 1  # assume a single L3NM service has been created
-    service_uuid = set(service_uuids).pop()
-
-    # Delete Connectivity Service
-    osm_wim.delete_connectivity_service(service_uuid)
-
-    # Verify the scenario has no services/slices
-    response = context_client.GetContext(ADMIN_CONTEXT_ID)
-    assert len(response.service_ids) == 0
-    assert len(response.slice_ids) == 0
-
-    # Load descriptors and validate the base scenario
-    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
-    descriptor_loader.validate()
diff --git a/src/tests/ofc24/tests/test_functional_delete_service_bidir.py b/src/tests/ofc24/tests/test_functional_delete_service_bidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..a337336a87535a89aa6ca176d56e40d33dcb1aca
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_delete_service_bidir.py
@@ -0,0 +1,78 @@
+# 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 logging
+from typing import Set, Tuple
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceId, ServiceStatusEnum, ServiceTypeEnum
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from common.tools.object_factory.Service import json_service_id
+from context.client.ContextClient import ContextClient
+from service.client.ServiceClient import ServiceClient
+from tests.Fixtures import context_client, service_client   # pylint: disable=unused-import
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_removal_bidir(
+    context_client : ContextClient, # pylint: disable=redefined-outer-name
+    service_client : ServiceClient, # pylint: disable=redefined-outer-name
+):
+    # Verify the scenario has 1 service and 0 slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 1
+    assert len(response.slice_ids) == 0
+
+    # Check there are no slices
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 0
+
+    # Check there is 1 service
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 1
+
+    context_service_uuids : Set[Tuple[str, str]] = set()
+    for service in response.services:
+        service_id = service.service_id
+        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE
+
+        response = context_client.ListConnections(service_id)
+        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
+            assert len(response.connections) == 2
+            context_uuid = service_id.context_id.context_uuid.uuid
+            service_uuid = service_id.service_uuid.uuid
+            context_service_uuids.add((context_uuid, service_uuid))
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
+
+    # Identify service to delete
+    assert len(context_service_uuids) == 1
+    context_uuid, service_uuid = set(context_service_uuids).pop()
+
+    # Delete Service
+    service_client.DeleteService(ServiceId(**json_service_id(service_uuid, json_context_id(context_uuid))))
+
+    # Verify the scenario has no services/slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 0
+    assert len(response.slice_ids) == 0
diff --git a/src/tests/ofc24/tests/test_functional_delete_service_unidir.py b/src/tests/ofc24/tests/test_functional_delete_service_unidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b0381c492bd1c0783aaf9872037bfefdd25fa37
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_delete_service_unidir.py
@@ -0,0 +1,78 @@
+# 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 logging
+from typing import Set, Tuple
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceId, ServiceStatusEnum, ServiceTypeEnum
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from common.tools.object_factory.Service import json_service_id
+from context.client.ContextClient import ContextClient
+from service.client.ServiceClient import ServiceClient
+from tests.Fixtures import context_client, service_client   # pylint: disable=unused-import
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_removal_unidir(
+    context_client : ContextClient, # pylint: disable=redefined-outer-name
+    service_client : ServiceClient, # pylint: disable=redefined-outer-name
+):
+    # Verify the scenario has 1 service and 0 slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 1
+    assert len(response.slice_ids) == 0
+
+    # Check there are no slices
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 0
+
+    # Check there is 1 service
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.warning('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 1
+
+    context_service_uuids : Set[Tuple[str, str]] = set()
+    for service in response.services:
+        service_id = service.service_id
+        assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE
+
+        response = context_client.ListConnections(service_id)
+        LOGGER.warning('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY:
+            assert len(response.connections) == 2
+            context_uuid = service_id.context_id.context_uuid.uuid
+            service_uuid = service_id.service_uuid.uuid
+            context_service_uuids.add((context_uuid, service_uuid))
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
+
+    # Identify service to delete
+    assert len(context_service_uuids) == 1
+    context_uuid, service_uuid = set(context_service_uuids).pop()
+
+    # Delete Service
+    service_client.DeleteService(ServiceId(**json_service_id(service_uuid, json_context_id(context_uuid))))
+
+    # Verify the scenario has no services/slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 0
+    assert len(response.slice_ids) == 0