diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 02d30d347ff9c4d5e6ec63cbc8b18ca5d97dbd23..329f321f8f1ea742476936773c3ce84d61431ca6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,6 +52,8 @@ include:
   #- local: '/src/telemetry/.gitlab-ci.yml'
   #- local: '/src/analytics/.gitlab-ci.yml'
   #- local: '/src/qos_profile/.gitlab-ci.yml'
+  #- local: '/src/vnt_manager/.gitlab-ci.yml'
+  #- local: '/src/e2e_orchestrator/.gitlab-ci.yml'
 
   # This should be last one: end-to-end integration tests
   - local: '/src/tests/.gitlab-ci.yml'
diff --git a/deploy/crdb.sh b/deploy/crdb.sh
index 3fcbb5cfaa1cbfcfdef14e372a871a7dd545887b..42b49fe984d08c8fb2cae14e68f0a6d2a7a726dd 100755
--- a/deploy/crdb.sh
+++ b/deploy/crdb.sh
@@ -175,6 +175,7 @@ function crdb_drop_databases_single() {
             --execute "SHOW DATABASES;" --format=tsv | awk '{print $1}' | grep "^tfs"
     )
     echo "Found TFS databases: ${DATABASES}" | tr '\n' ' '
+    echo
 
     for DB_NAME in $DATABASES; do
         echo "Dropping TFS database: $DB_NAME"
@@ -369,6 +370,7 @@ function crdb_drop_databases_cluster() {
             --execute "SHOW DATABASES;" --format=tsv | awk '{print $1}' | grep "^tfs"
     )
     echo "Found TFS databases: ${DATABASES}" | tr '\n' ' '
+    echo
 
     for DB_NAME in $DATABASES; do
         echo "Dropping TFS database: $DB_NAME"
diff --git a/deploy/tfs.sh b/deploy/tfs.sh
index 65c1e8de28f2045b2ac78938b84d3c33e282025e..6c0ddcb63e71abd2e713e4809aeb7795b43053fa 100755
--- a/deploy/tfs.sh
+++ b/deploy/tfs.sh
@@ -344,11 +344,10 @@ for COMPONENT in $TFS_COMPONENTS; do
         VERSION=$(grep -i "${GITLAB_REPO_URL}/${COMPONENT}-gateway:" "$MANIFEST" | cut -d ":" -f4)
         sed -E -i "s#image: $GITLAB_REPO_URL/$COMPONENT-gateway:${VERSION}#image: $IMAGE_URL#g" "$MANIFEST"
     else
+        VERSION=$(grep -i "${GITLAB_REPO_URL}/${COMPONENT}:" "$MANIFEST" | cut -d ":" -f4)
         if [ "$TFS_SKIP_BUILD" != "YES" ]; then
             IMAGE_URL=$(echo "$TFS_REGISTRY_IMAGES/$COMPONENT:$TFS_IMAGE_TAG" | sed 's,//,/,g' | sed 's,http:/,,g')
-            VERSION=$(grep -i "${GITLAB_REPO_URL}/${COMPONENT}:" "$MANIFEST" | cut -d ":" -f4)
         else
-            VERSION=$(grep -i "${GITLAB_REPO_URL}/${COMPONENT}:" "$MANIFEST" | cut -d ":" -f4)
             IMAGE_URL=$(echo "$TFS_REGISTRY_IMAGES/$COMPONENT:$VERSION" | sed 's,//,/,g' | sed 's,http:/,,g')
         fi
         sed -E -i "s#image: $GITLAB_REPO_URL/$COMPONENT:${VERSION}#image: $IMAGE_URL#g" "$MANIFEST"
diff --git a/ecoc24 b/ecoc24
new file mode 120000
index 0000000000000000000000000000000000000000..37c97d3a77727ec098303cc9a5e04d711a30fcec
--- /dev/null
+++ b/ecoc24
@@ -0,0 +1 @@
+src/tests/ecoc24/
\ No newline at end of file
diff --git a/manifests/analyticsservice.yaml b/manifests/analyticsservice.yaml
index 61666ead951c73e4034110b00a51743d33bd4ce2..536bb185286ba5444ad22d17d00706a066172e4c 100644
--- a/manifests/analyticsservice.yaml
+++ b/manifests/analyticsservice.yaml
@@ -98,11 +98,11 @@ spec:
   selector:
     app: analyticsservice
   ports:
-    - name: frontend-grpc
+    - name: grpc
       protocol: TCP
       port: 30080
       targetPort: 30080
-    - name: backend-grpc
+    - name: grpc-backend
       protocol: TCP
       port: 30090
       targetPort: 30090
diff --git a/manifests/e2e_orchestratorservice.yaml b/manifests/e2e_orchestratorservice.yaml
index b6354c7a8aef7dc565b61d84703f905055b9303f..5f70fdfdac08112650afc7bc50f02a7eedd5a30a 100644
--- a/manifests/e2e_orchestratorservice.yaml
+++ b/manifests/e2e_orchestratorservice.yaml
@@ -20,8 +20,12 @@ spec:
   selector:
     matchLabels:
       app: e2e-orchestratorservice
+  replicas: 1
   template:
     metadata:
+      annotations:
+        config.linkerd.io/skip-outbound-ports: "8761"
+        config.linkerd.io/skip-inbound-ports: "8761"
       labels:
         app: e2e-orchestratorservice
     spec:
@@ -33,9 +37,18 @@ spec:
           ports:
             - containerPort: 10050
             - containerPort: 9192
+            - containerPort: 8761
           env:
             - name: LOG_LEVEL
               value: "INFO"
+            - name: WS_IP_HOST
+              value: "nbiservice.tfs-ip.svc.cluster.local"
+            - name: WS_IP_PORT
+              value: 8761
+            - name: WS_E2E_HOST
+              value: "e2e-orchestratorservice.tfs-e2e.svc.cluster.local"
+            - name: WS_E2E_PORT
+              value: 8762
           readinessProbe:
             exec:
               command: ["/bin/grpc_health_probe", "-addr=:10050"]
@@ -67,25 +80,6 @@ spec:
     - name: metrics
       port: 9192
       targetPort: 9192
----
-apiVersion: autoscaling/v2
-kind: HorizontalPodAutoscaler
-metadata:
-  name: e2e-orchestratorservice-hpa
-spec:
-  scaleTargetRef:
-    apiVersion: apps/v1
-    kind: Deployment
-    name: e2e-orchestratorservice
-  minReplicas: 1
-  maxReplicas: 20
-  metrics:
-    - type: Resource
-      resource:
-        name: cpu
-        target:
-          type: Utilization
-          averageUtilization: 80
-  #behavior:
-  #  scaleDown:
-  #    stabilizationWindowSeconds: 30
+    - name: ws
+      port: 8761
+      targetPort: 8761
diff --git a/manifests/nbiservice.yaml b/manifests/nbiservice.yaml
index 70f553e6425ca7972b8af185f432842b4e184790..a514736c14a9bdd1f64301e5566719bb5ee5c0c7 100644
--- a/manifests/nbiservice.yaml
+++ b/manifests/nbiservice.yaml
@@ -23,6 +23,9 @@ spec:
   replicas: 1
   template:
     metadata:
+      annotations:
+        config.linkerd.io/skip-inbound-ports: "8762"
+        config.linkerd.io/skip-outbound-ports: "8762"
       labels:
         app: nbiservice
     spec:
@@ -35,11 +38,14 @@ spec:
             - containerPort: 8080
             - containerPort: 9090
             - containerPort: 9192
+            - containerPort: 8762
           env:
             - name: LOG_LEVEL
               value: "INFO"
             - name: IETF_NETWORK_RENDERER
               value: "LIBYANG"
+            - name: WS_E2E_PORT
+              value: 8762
           readinessProbe:
             exec:
               command: ["/bin/grpc_health_probe", "-addr=:9090"]
@@ -77,3 +83,7 @@ spec:
       protocol: TCP
       port: 9192
       targetPort: 9192
+    - name: ws
+      protocol: TCP
+      port: 8762
+      targetPort: 8762
diff --git a/manifests/telemetryservice.yaml b/manifests/telemetryservice.yaml
index cd35d2698816bcfc5bc2030506eb2897a85708f6..86d864157838513dd68f10679d44d11b074c422c 100644
--- a/manifests/telemetryservice.yaml
+++ b/manifests/telemetryservice.yaml
@@ -98,11 +98,11 @@ spec:
   selector:
     app: telemetryservice
   ports:
-    - name: frontend-grpc
+    - name: grpc
       protocol: TCP
       port: 30050
       targetPort: 30050
-    - name: backend-grpc
+    - name: grpc-backend
       protocol: TCP
       port: 30060
       targetPort: 30060
diff --git a/manifests/vnt_managerservice.yaml b/manifests/vnt_managerservice.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2e31c743902da3dd6ab9354f5b7617d4e884751e
--- /dev/null
+++ b/manifests/vnt_managerservice.yaml
@@ -0,0 +1,77 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: vnt-managerservice
+spec:
+  selector:
+    matchLabels:
+      app: vnt-managerservice
+  replicas: 1
+  template:
+    metadata:
+      annotations:
+        config.linkerd.io/skip-outbound-ports: "8765"
+        config.linkerd.io/skip-inbound-ports: "8765"
+      labels:
+        app: vnt-managerservice
+    spec:
+      terminationGracePeriodSeconds: 5
+      containers:
+        - name: server
+          image: labs.etsi.org:5050/tfs/controller/vnt_manager:latest
+          imagePullPolicy: Always
+          ports:
+            - containerPort: 10080
+            - containerPort: 9192
+          env:
+            - name: LOG_LEVEL
+              value: "INFO"
+            - name: WS_IP_PORT
+              value: 8761
+            - name: WS_E2E_PORT
+              value: 8762
+          readinessProbe:
+            exec:
+              command: ["/bin/grpc_health_probe", "-addr=:10080"]
+          livenessProbe:
+            exec:
+              command: ["/bin/grpc_health_probe", "-addr=:10080"]
+          resources:
+            requests:
+              cpu: 250m
+              memory: 128Mi
+            limits:
+              cpu: 1000m
+              memory: 1024Mi
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: vnt-managerservice
+  labels:
+    app: vnt-managerservice
+spec:
+  type: ClusterIP
+  selector:
+    app: vnt-managerservice
+  ports:
+    - name: grpc
+      port: 10080
+      targetPort: 10080
+    - name: metrics
+      port: 9192
+      targetPort: 9192
diff --git a/my_deploy.sh b/my_deploy.sh
index 344ca44ee335e73dcc7b8f8c9ca71ead7d90880f..8d2e733d462ae743c7187eb9b6a58d7da14033a7 100755
--- a/my_deploy.sh
+++ b/my_deploy.sh
@@ -20,7 +20,7 @@
 export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/"
 
 # Set the list of components, separated by spaces, you want to build images for, and deploy.
-export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_generator"
+export TFS_COMPONENTS="context device pathcomp service slice nbi webui"
 
 # Uncomment to activate Monitoring (old)
 #export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring"
@@ -65,6 +65,9 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_gene
 # Uncomment to activate E2E Orchestrator
 #export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"
 
+# Uncomment to activate VNT Manager
+#export TFS_COMPONENTS="${TFS_COMPONENTS} vnt_manager"
+
 # Uncomment to activate DLT and Interdomain
 #export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt"
 #if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then
@@ -83,6 +86,9 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_gene
 #    export TFS_COMPONENTS="${BEFORE} qkd_app service ${AFTER}"
 #fi
 
+# Uncomment to activate Load Generator
+#export TFS_COMPONENTS="${TFS_COMPONENTS} load_generator"
+
 
 # Set the tag you want to use for your images.
 export TFS_IMAGE_TAG="dev"
diff --git a/proto/context.proto b/proto/context.proto
index 85972d956a93dfe09ec9a955cf304c8b3e298bb3..d3888e77b0256f63fb4b4eb10b987137057a0aa7 100644
--- a/proto/context.proto
+++ b/proto/context.proto
@@ -260,6 +260,7 @@ message Link {
   string name = 2;
   repeated EndPointId link_endpoint_ids = 3;
   LinkAttributes attributes = 4;
+  LinkTypeEnum link_type = 5;
 }
 
 message LinkIdList {
@@ -275,6 +276,13 @@ message LinkEvent {
   LinkId link_id = 2;
 }
 
+enum LinkTypeEnum {
+  LINKTYPE_UNKNOWN = 0;
+  LINKTYPE_COPPER = 1;
+  LINKTYPE_VIRTUAL_COPPER = 2;
+  LINKTYPE_OPTICAL = 3;
+  LINKTYPE_VIRTUAL_OPTICAL = 4;
+}
 
 // ----- Service -------------------------------------------------------------------------------------------------------
 message ServiceId {
diff --git a/proto/e2eorchestrator.proto b/proto/e2eorchestrator.proto
index d8f539f9fa7a7adaeaf48add127dd3077c904375..731016934300a45e6664b90e73ba458cb03ecfb7 100644
--- a/proto/e2eorchestrator.proto
+++ b/proto/e2eorchestrator.proto
@@ -20,7 +20,8 @@ import "context.proto";
 
 
 service E2EOrchestratorService {
-  rpc Compute(E2EOrchestratorRequest) returns (E2EOrchestratorReply) {}
+  rpc Compute(E2EOrchestratorRequest) returns (E2EOrchestratorReply)  {}
+  rpc PushTopology(context.Topology)  returns (context.Empty)         {}
 }
 
 message E2EOrchestratorRequest {
diff --git a/proto/vnt_manager.proto b/proto/vnt_manager.proto
new file mode 100644
index 0000000000000000000000000000000000000000..1e1d67e122fa0cbb7088b366c5001a712a38a1f0
--- /dev/null
+++ b/proto/vnt_manager.proto
@@ -0,0 +1,37 @@
+// Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+// protocol buffers documentation: https://developers.google.com/protocol-buffers/docs/proto3
+syntax = "proto3";
+package vnt_manager;
+import "context.proto";
+
+
+service VNTManagerService {
+  rpc VNTSubscript          (VNTSubscriptionRequest)    returns (VNTSubscriptionReply)  {}
+  rpc ListVirtualLinkIds    (context.Empty)             returns (context.LinkIdList)    {}
+  rpc ListVirtualLinks      (context.Empty)             returns (context.LinkList)      {}
+  rpc GetVirtualLink        (context.LinkId)            returns (context.Link)          {}
+  rpc SetVirtualLink        (context.Link)              returns (context.LinkId)        {}
+  rpc RemoveVirtualLink     (context.LinkId)            returns (context.Empty)         {}
+}
+
+message VNTSubscriptionRequest {
+  string host = 1;
+  string port = 2;
+}
+
+message VNTSubscriptionReply {
+  string subscription = 1;
+}
diff --git a/scripts/show_logs_automation.sh b/scripts/show_logs_automation.sh
index 8a0e417d9a7ddf1ffe0b4e4529606683ae600ecd..26684298091403f4dc737fc0d1ca5b05d82ad374 100755
--- a/scripts/show_logs_automation.sh
+++ b/scripts/show_logs_automation.sh
@@ -24,4 +24,4 @@ export TFS_K8S_NAMESPACE=${TFS_K8S_NAMESPACE:-"tfs"}
 # Automated steps start here
 ########################################################################################################################
 
-kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/ztpservice
+kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/automationservice
diff --git a/scripts/show_logs_e2eorchestrator.sh b/scripts/show_logs_e2eorchestrator.sh
old mode 100644
new mode 100755
index 5ac39c6cba06e57720368dd37454986fdc0e1e29..bf1fb5987e4a65fd231f057f71cc3ffcffc14655
--- a/scripts/show_logs_e2eorchestrator.sh
+++ b/scripts/show_logs_e2eorchestrator.sh
@@ -24,4 +24,4 @@ export TFS_K8S_NAMESPACE=${TFS_K8S_NAMESPACE:-"tfs"}
 # Automated steps start here
 ########################################################################################################################
 
-kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/e2eorchestratorservice -c server
+kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/e2e-orchestratorservice -c server
diff --git a/scripts/show_logs_vntmanager.sh b/scripts/show_logs_vntmanager.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0dba86567bd5118b685c72fd17b1904acfe698cc
--- /dev/null
+++ b/scripts/show_logs_vntmanager.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+########################################################################################################################
+# Define your deployment settings here
+########################################################################################################################
+
+# If not already set, set the name of the Kubernetes namespace to deploy to.
+export TFS_K8S_NAMESPACE=${TFS_K8S_NAMESPACE:-"tfs"}
+
+########################################################################################################################
+# Automated steps start here
+########################################################################################################################
+
+kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/vnt_managerservice -c server
diff --git a/scripts/update_license_headers.py b/scripts/update_license_headers.py
index 1de9f9b7c2795505aba53276a0deb2acdcccc703..34d620ed8bc1a0817509916c2d1f857979cdd431 100644
--- a/scripts/update_license_headers.py
+++ b/scripts/update_license_headers.py
@@ -32,6 +32,7 @@ STR_NEW_COPYRIGHT = 'Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https:/
 RE_OLD_COPYRIGHTS = [
     r'Copyright\ 2021\-2023\ H2020\ TeraFlow\ \(https\:\/\/www\.teraflow\-h2020\.eu\/\)',
     r'Copyright\ 2022\-2023\ ETSI\ TeraFlowSDN\ \-\ TFS\ OSG\ \(https\:\/\/tfs\.etsi\.org\/\)',
+    r'Copyright\ 2022\-2024\ ETSI\ TeraFlowSDN\ \-\ TFS\ OSG\ \(https\:\/\/tfs\.etsi\.org\/\)',
 ]
 RE_OLD_COPYRIGHTS = [
     (re.compile(r'.*{}.*'.format(re_old_copyright)), re.compile(re_old_copyright))
diff --git a/src/analytics/frontend/client/AnalyticsFrontendClient.py b/src/analytics/frontend/client/AnalyticsFrontendClient.py
index 90e95d661d46f24ae5ffaeb7bcfa19b7e1f36526..809c957ea48a07a657fe1edc244c9c0f125e9058 100644
--- a/src/analytics/frontend/client/AnalyticsFrontendClient.py
+++ b/src/analytics/frontend/client/AnalyticsFrontendClient.py
@@ -28,8 +28,8 @@ RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION,
 
 class AnalyticsFrontendClient:
     def __init__(self, host=None, port=None):
-        if not host: host = get_service_host(ServiceNameEnum.ANALYTICSFRONTEND)
-        if not port: port = get_service_port_grpc(ServiceNameEnum.ANALYTICSFRONTEND)
+        if not host: host = get_service_host(ServiceNameEnum.ANALYTICS)
+        if not port: port = get_service_port_grpc(ServiceNameEnum.ANALYTICS)
         self.endpoint     = '{:s}:{:s}'.format(str(host), str(port))
         LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint)))
         self.channel      = None
diff --git a/src/analytics/frontend/service/AnalyticsFrontendService.py b/src/analytics/frontend/service/AnalyticsFrontendService.py
index 42a7fc9b60418c1c0fc5af6f320ae5c330ce8871..8d2536fe091459d6026941f4eae52f58f7cd3f3a 100644
--- a/src/analytics/frontend/service/AnalyticsFrontendService.py
+++ b/src/analytics/frontend/service/AnalyticsFrontendService.py
@@ -20,7 +20,7 @@ from analytics.frontend.service.AnalyticsFrontendServiceServicerImpl import Anal
 
 class AnalyticsFrontendService(GenericGrpcService):
     def __init__(self, cls_name: str = __name__):
-        port = get_service_port_grpc(ServiceNameEnum.ANALYTICSFRONTEND)
+        port = get_service_port_grpc(ServiceNameEnum.ANALYTICS)
         super().__init__(port, cls_name=cls_name)
         self.analytics_frontend_servicer = AnalyticsFrontendServiceServicerImpl()
     
diff --git a/src/analytics/frontend/tests/test_frontend.py b/src/analytics/frontend/tests/test_frontend.py
index 48ab4dac5a5dfbdec688fc5c346f95d41e32c81c..74fef6c79cc2328b65671b392220ae86106e9d5d 100644
--- a/src/analytics/frontend/tests/test_frontend.py
+++ b/src/analytics/frontend/tests/test_frontend.py
@@ -41,9 +41,9 @@ from apscheduler.triggers.interval                       import IntervalTrigger
 
 LOCAL_HOST = '127.0.0.1'
 
-ANALYTICS_FRONTEND_PORT = str(get_service_port_grpc(ServiceNameEnum.ANALYTICSFRONTEND))
-os.environ[get_env_var_name(ServiceNameEnum.ANALYTICSFRONTEND, ENVVAR_SUFIX_SERVICE_HOST     )] = str(LOCAL_HOST)
-os.environ[get_env_var_name(ServiceNameEnum.ANALYTICSFRONTEND, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(ANALYTICS_FRONTEND_PORT)
+ANALYTICS_FRONTEND_PORT = str(get_service_port_grpc(ServiceNameEnum.ANALYTICS))
+os.environ[get_env_var_name(ServiceNameEnum.ANALYTICS, ENVVAR_SUFIX_SERVICE_HOST     )] = str(LOCAL_HOST)
+os.environ[get_env_var_name(ServiceNameEnum.ANALYTICS, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(ANALYTICS_FRONTEND_PORT)
 
 LOGGER = logging.getLogger(__name__)
 
diff --git a/src/automation/service/EventEngine.py b/src/automation/service/EventEngine.py
new file mode 100644
index 0000000000000000000000000000000000000000..26c2b28cbe35230beec90dd9df4112d4ad131876
--- /dev/null
+++ b/src/automation/service/EventEngine.py
@@ -0,0 +1,169 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json, logging, queue, threading
+from typing import Dict, Optional
+from automation.service.Tools import create_kpi_descriptor, start_collector
+from common.proto.context_pb2 import (
+    ConfigActionEnum, DeviceEvent, DeviceOperationalStatusEnum, Empty, ServiceEvent
+)
+from common.proto.kpi_sample_types_pb2 import KpiSampleType
+from common.tools.grpc.BaseEventCollector import BaseEventCollector
+from common.tools.grpc.BaseEventDispatcher import BaseEventDispatcher
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from context.client.ContextClient import ContextClient
+from kpi_manager.client.KpiManagerClient import KpiManagerClient
+from telemetry.frontend.client.TelemetryFrontendClient import TelemetryFrontendClient
+
+LOGGER = logging.getLogger(__name__)
+
+DEVICE_OP_STATUS_UNDEFINED   = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED
+DEVICE_OP_STATUS_DISABLED    = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_DISABLED
+DEVICE_OP_STATUS_ENABLED     = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED
+DEVICE_OP_STATUS_NOT_ENABLED = {DEVICE_OP_STATUS_UNDEFINED, DEVICE_OP_STATUS_DISABLED}
+
+KPISAMPLETYPE_UNKNOWN        = KpiSampleType.KPISAMPLETYPE_UNKNOWN
+
+class EventCollector(BaseEventCollector):
+    pass
+
+class EventDispatcher(BaseEventDispatcher):
+    def __init__(
+        self, events_queue : queue.PriorityQueue,
+        terminate : Optional[threading.Event] = None
+    ) -> None:
+        super().__init__(events_queue, terminate)
+        self._context_client     = ContextClient()
+        self._kpi_manager_client = KpiManagerClient()
+        self._telemetry_client   = TelemetryFrontendClient()
+        self._device_endpoint_monitored : Dict[str, Dict[str, bool]] = dict()
+
+    def dispatch_device_create(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Create: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+        self._device_activate_monitoring(device_event)
+
+    def dispatch_device_update(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Update: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+        self._device_activate_monitoring(device_event)
+
+    def dispatch_device_remove(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_service_create(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Create: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+    def dispatch_service_update(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Update: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+    def dispatch_service_remove(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+    def _device_activate_monitoring(self, device_event : DeviceEvent) -> None:
+        device_id = device_event.device_id
+        device_uuid = device_id.device_uuid.uuid
+        device = self._context_client.GetDevice(device_id)
+
+        device_op_status = device.device_operational_status
+        if device_op_status != DEVICE_OP_STATUS_ENABLED:
+            LOGGER.debug('Ignoring Device not enabled: {:s}'.format(grpc_message_to_json_string(device)))
+            return
+
+        enabled_endpoint_names = set()
+        for config_rule in device.device_config.config_rules:
+            if config_rule.action != ConfigActionEnum.CONFIGACTION_SET: continue
+            if config_rule.WhichOneof('config_rule') != 'custom': continue
+            str_resource_key = str(config_rule.custom.resource_key)
+            if not str_resource_key.startswith('/interface['): continue
+            json_resource_value = json.loads(config_rule.custom.resource_value)
+            if 'name' not in json_resource_value: continue
+            if 'enabled' not in json_resource_value: continue
+            if not json_resource_value['enabled']: continue
+            enabled_endpoint_names.add(json_resource_value['name'])
+
+        endpoints_monitored = self._device_endpoint_monitored.setdefault(device_uuid, dict())
+        for endpoint in device.device_endpoints:
+            endpoint_uuid = endpoint.endpoint_id.endpoint_uuid.uuid
+            endpoint_name_or_uuid = endpoint.name
+            if endpoint_name_or_uuid is None or len(endpoint_name_or_uuid) == 0:
+                endpoint_name_or_uuid = endpoint_uuid
+
+            endpoint_was_monitored = endpoints_monitored.get(endpoint_uuid, False)
+            endpoint_is_enabled = (endpoint_name_or_uuid in enabled_endpoint_names)
+
+            if not endpoint_was_monitored and endpoint_is_enabled:
+                # activate
+                for kpi_sample_type in endpoint.kpi_sample_types:
+                    if kpi_sample_type == KPISAMPLETYPE_UNKNOWN: continue
+
+                    kpi_id = create_kpi_descriptor(
+                        self._kpi_manager_client, kpi_sample_type,
+                        device_id=device.device_id,
+                        endpoint_id=endpoint.endpoint_id,
+                    )
+
+                    duration_seconds = 86400
+                    interval_seconds = 10
+                    collector_id = start_collector(
+                        self._telemetry_client, kpi_id,
+                        duration_seconds, interval_seconds
+                    )
+
+                endpoints_monitored[endpoint_uuid] = True
+            else:
+                MSG = 'Not implemented condition: event={:s} device={:s} endpoint={:s}' + \
+                        ' endpoint_was_monitored={:s} endpoint_is_enabled={:s}'
+                LOGGER.warning(MSG.format(
+                    grpc_message_to_json_string(device_event), grpc_message_to_json_string(device),
+                    grpc_message_to_json_string(endpoint), str(endpoint_was_monitored),
+                    str(endpoint_is_enabled)
+                ))
+
+class EventEngine:
+    def __init__(
+        self, terminate : Optional[threading.Event] = None
+    ) -> None:
+        self._terminate = threading.Event() if terminate is None else terminate
+
+        self._context_client = ContextClient()
+        self._event_collector = EventCollector(terminate=self._terminate)
+        self._event_collector.install_collector(
+            self._context_client.GetDeviceEvents, Empty(),
+            log_events_received=True
+        )
+        self._event_collector.install_collector(
+            self._context_client.GetServiceEvents, Empty(),
+            log_events_received=True
+        )
+
+        self._event_dispatcher = EventDispatcher(
+            self._event_collector.get_events_queue(),
+            terminate=self._terminate
+        )
+
+    def start(self) -> None:
+        self._context_client.connect()
+        self._event_collector.start()
+        self._event_dispatcher.start()
+
+    def stop(self) -> None:
+        self._terminate.set()
+        self._event_dispatcher.stop()
+        self._event_collector.stop()
+        self._context_client.close()
diff --git a/src/automation/service/Tools.py b/src/automation/service/Tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a63475ca23c576a6fe946d6d149b70465ff1e1f
--- /dev/null
+++ b/src/automation/service/Tools.py
@@ -0,0 +1,64 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, uuid
+from typing import Optional
+from common.proto.context_pb2 import ConnectionId, DeviceId, EndPointId, LinkId, ServiceId, SliceId
+from common.proto.kpi_manager_pb2 import KpiDescriptor, KpiId
+from common.proto.kpi_sample_types_pb2 import KpiSampleType
+from common.proto.telemetry_frontend_pb2 import Collector, CollectorId
+from kpi_manager.client.KpiManagerClient import KpiManagerClient
+from telemetry.frontend.client.TelemetryFrontendClient import TelemetryFrontendClient
+
+LOGGER = logging.getLogger(__name__)
+
+def create_kpi_descriptor(
+    kpi_manager_client : KpiManagerClient,
+    kpi_sample_type    : KpiSampleType,
+    device_id          : Optional[DeviceId    ] = None,
+    endpoint_id        : Optional[EndPointId  ] = None,
+    service_id         : Optional[ServiceId   ] = None,
+    slice_id           : Optional[SliceId     ] = None,
+    connection_id      : Optional[ConnectionId] = None,
+    link_id            : Optional[LinkId      ] = None,
+) -> KpiId:
+    kpi_descriptor = KpiDescriptor()
+    kpi_descriptor.kpi_id.kpi_id.uuid = str(uuid.uuid4())
+    kpi_descriptor.kpi_description = ''
+    kpi_descriptor.kpi_sample_type = kpi_sample_type
+
+    if device_id     is not None: kpi_descriptor.device_id    .CopyFrom(device_id    )
+    if endpoint_id   is not None: kpi_descriptor.endpoint_id  .CopyFrom(endpoint_id  )
+    if service_id    is not None: kpi_descriptor.service_id   .CopyFrom(service_id   )
+    if slice_id      is not None: kpi_descriptor.slice_id     .CopyFrom(slice_id     )
+    if connection_id is not None: kpi_descriptor.connection_id.CopyFrom(connection_id)
+    if link_id       is not None: kpi_descriptor.link_id      .CopyFrom(link_id      )
+
+    kpi_id : KpiId = kpi_manager_client.SetKpiDescriptor(kpi_descriptor)
+    return kpi_id
+
+def start_collector(
+    telemetry_client : TelemetryFrontendClient,
+    kpi_id : KpiId,
+    duration_seconds : float,
+    interval_seconds : float
+) -> CollectorId:
+    collector = Collector()
+    collector.collector_id.collector_id.uuid = str(uuid.uuid4())
+    collector.kpi_id.CopyFrom(kpi_id)
+    collector.duration_s = duration_seconds
+    collector.interval_s = interval_seconds
+    collector_id : CollectorId = telemetry_client.StartCollector(collector)
+    return collector_id
diff --git a/src/automation/service/__main__.py b/src/automation/service/__main__.py
index 39d8beaffd959744b83d7e1ace78da2b1b800a21..3baa0bd30b19fb624c5dcf0b236642704e42ab9f 100644
--- a/src/automation/service/__main__.py
+++ b/src/automation/service/__main__.py
@@ -14,7 +14,13 @@
 
 import logging, signal, sys, threading
 from prometheus_client import start_http_server
-from common.Settings import get_log_level, get_metrics_port
+from automation.service.EventEngine import EventEngine
+from common.Constants import ServiceNameEnum
+from common.Settings import (
+    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC,
+    get_env_var_name, get_log_level, get_metrics_port,
+    wait_for_environment_variables
+)
 from .AutomationService import AutomationService
 
 LOG_LEVEL = get_log_level()
@@ -29,6 +35,22 @@ def signal_handler(signal, frame): # pylint: disable=redefined-outer-name,unused
 
 def main():
     LOGGER.info('Starting...')
+
+    wait_for_environment_variables([
+        get_env_var_name(ServiceNameEnum.CONTEXT,    ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.CONTEXT,    ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.DEVICE,     ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.DEVICE,     ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.KPIMANAGER, ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.KPIMANAGER, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.TELEMETRY,  ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.TELEMETRY,  ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.ANALYTICS,  ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.ANALYTICS,  ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.POLICY,     ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.POLICY,     ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+    ])
+
     signal.signal(signal.SIGINT,  signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
 
@@ -36,7 +58,11 @@ def main():
     metrics_port = get_metrics_port()
     start_http_server(metrics_port)
 
-    # Starting context service
+    # Start Event Collection+Dispatching Engine
+    event_engine = EventEngine(terminate=terminate)
+    event_engine.start()
+
+    # Starting Automation service
     grpc_service = AutomationService()
     grpc_service.start()
 
@@ -45,6 +71,7 @@ def main():
 
     LOGGER.info('Terminating...')
     grpc_service.stop()
+    event_engine.stop()
 
     LOGGER.info('Bye')
     return 0
diff --git a/src/common/Constants.py b/src/common/Constants.py
index 33a5d5047c8969118417a8b06a13cb8fb6fbf7df..aa15d66c5937c3f5a589ddf9cbb432d08c4b49b0 100644
--- a/src/common/Constants.py
+++ b/src/common/Constants.py
@@ -61,14 +61,15 @@ class ServiceNameEnum(Enum):
     FORECASTER             = 'forecaster'
     E2EORCHESTRATOR        = 'e2e-orchestrator'
     OPTICALCONTROLLER      = 'opticalcontroller'
+    VNTMANAGER             = 'vnt-manager'
     BGPLS                  = 'bgpls-speaker'
     QKD_APP                = 'qkd_app'
     KPIMANAGER             = 'kpi-manager'
     KPIVALUEAPI            = 'kpi-value-api'
     KPIVALUEWRITER         = 'kpi-value-writer'
-    TELEMETRYFRONTEND      = 'telemetry-frontend'
+    TELEMETRY              = 'telemetry'
     TELEMETRYBACKEND       = 'telemetry-backend'
-    ANALYTICSFRONTEND      = 'analytics-frontend'
+    ANALYTICS              = 'analytics'
     ANALYTICSBACKEND       = 'analytics-backend'
     QOSPROFILE             = 'qos-profile'
 
@@ -100,14 +101,15 @@ DEFAULT_SERVICE_GRPC_PORTS = {
     ServiceNameEnum.E2EORCHESTRATOR        .value : 10050,
     ServiceNameEnum.OPTICALCONTROLLER      .value : 10060,
     ServiceNameEnum.QKD_APP                .value : 10070,
+    ServiceNameEnum.VNTMANAGER             .value : 10080,
     ServiceNameEnum.BGPLS                  .value : 20030,
     ServiceNameEnum.QOSPROFILE             .value : 20040,
     ServiceNameEnum.KPIMANAGER             .value : 30010,
     ServiceNameEnum.KPIVALUEAPI            .value : 30020,
     ServiceNameEnum.KPIVALUEWRITER         .value : 30030,
-    ServiceNameEnum.TELEMETRYFRONTEND      .value : 30050,
+    ServiceNameEnum.TELEMETRY              .value : 30050,
     ServiceNameEnum.TELEMETRYBACKEND       .value : 30060,
-    ServiceNameEnum.ANALYTICSFRONTEND      .value : 30080,
+    ServiceNameEnum.ANALYTICS              .value : 30080,
     ServiceNameEnum.ANALYTICSBACKEND       .value : 30090,
     ServiceNameEnum.AUTOMATION             .value : 30200,
 
diff --git a/src/common/tools/grpc/BaseEventCollector.py b/src/common/tools/grpc/BaseEventCollector.py
new file mode 100644
index 0000000000000000000000000000000000000000..04dfb654963da1ae4f83a8a14feaaa8c17d1f128
--- /dev/null
+++ b/src/common/tools/grpc/BaseEventCollector.py
@@ -0,0 +1,136 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+# See usage example below
+
+import grpc, logging, queue, threading, time
+from typing import Any, Callable, List, Optional
+from common.proto.context_pb2 import Empty
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from context.client.ContextClient import ContextClient
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+class CollectorThread(threading.Thread):
+    def __init__(
+        self, subscription_func : Callable, events_queue = queue.PriorityQueue,
+        terminate = threading.Event, log_events_received: bool = False
+    ) -> None:
+        super().__init__(daemon=False)
+        self._subscription_func = subscription_func
+        self._events_queue = events_queue
+        self._terminate = terminate
+        self._log_events_received = log_events_received
+        self._stream = None
+
+    def cancel(self) -> None:
+        if self._stream is None: return
+        self._stream.cancel()
+
+    def run(self) -> None:
+        while not self._terminate.is_set():
+            self._stream = self._subscription_func()
+            try:
+                for event in self._stream:
+                    if self._log_events_received:
+                        str_event = grpc_message_to_json_string(event)
+                        LOGGER.info('[_collect] event: {:s}'.format(str_event))
+                    timestamp = event.event.timestamp.timestamp
+                    self._events_queue.put_nowait((timestamp, event))
+            except grpc.RpcError as e:
+                if e.code() == grpc.StatusCode.UNAVAILABLE: # pylint: disable=no-member
+                    LOGGER.info('[_collect] UNAVAILABLE... retrying...')
+                    time.sleep(0.5)
+                    continue
+                elif e.code() == grpc.StatusCode.CANCELLED: # pylint: disable=no-member
+                    break
+                else:
+                    raise # pragma: no cover
+
+class BaseEventCollector:
+    def __init__(
+        self, terminate : Optional[threading.Event] = None
+    ) -> None:
+        self._events_queue = queue.PriorityQueue()
+        self._terminate = threading.Event() if terminate is None else terminate
+        self._collector_threads : List[CollectorThread] = list()
+
+    def install_collector(
+        self, subscription_method : Callable, request_message : Any,
+        log_events_received : bool = False
+    ) -> None:
+        self._collector_threads.append(CollectorThread(
+            lambda: subscription_method(request_message),
+            self._events_queue, self._terminate, log_events_received
+        ))
+
+    def start(self):
+        self._terminate.clear()
+        for collector_thread in self._collector_threads:
+            collector_thread.start()
+
+    def stop(self):
+        self._terminate.set()
+
+        for collector_thread in self._collector_threads:
+            collector_thread.cancel()
+
+        for collector_thread in self._collector_threads:
+            collector_thread.join()
+
+    def get_events_queue(self) -> queue.PriorityQueue:
+        return self._events_queue
+
+    def get_event(self, block : bool = True, timeout : float = 0.1):
+        try:
+            _,event = self._events_queue.get(block=block, timeout=timeout)
+            return event
+        except queue.Empty: # pylint: disable=catching-non-exception
+            return None
+
+    def get_events(self, block : bool = True, timeout : float = 0.1, count : int = None):
+        events = []
+        if count is None:
+            while not self._terminate.is_set():
+                event = self.get_event(block=block, timeout=timeout)
+                if event is None: break
+                events.append(event)
+        else:
+            while len(events) < count:
+                if self._terminate.is_set(): break
+                event = self.get_event(block=block, timeout=timeout)
+                if event is None: continue
+                events.append(event)
+        return sorted(events, key=lambda e: e.event.timestamp.timestamp)
+
+def main() -> None:
+    logging.basicConfig(level=logging.INFO)
+
+    context_client = ContextClient()
+    context_client.connect()
+
+    event_collector = BaseEventCollector()
+    event_collector.install_collector(context_client.GetDeviceEvents,  Empty(), log_events_received=True)
+    event_collector.install_collector(context_client.GetLinkEvents,    Empty(), log_events_received=True)
+    event_collector.install_collector(context_client.GetServiceEvents, Empty(), log_events_received=True)
+    event_collector.start()
+
+    time.sleep(60)
+
+    event_collector.stop()
+    context_client.close()
+
+if __name__ == '__main__':
+    main()
diff --git a/src/common/tools/grpc/BaseEventDispatcher.py b/src/common/tools/grpc/BaseEventDispatcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9ec292c994bb4728043bf3bfed73e176f4f748a
--- /dev/null
+++ b/src/common/tools/grpc/BaseEventDispatcher.py
@@ -0,0 +1,119 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+# See usage example below
+
+import logging, queue, threading, time
+from typing import Any, Callable, Optional
+from common.proto.context_pb2 import DeviceEvent, Empty, EventTypeEnum, LinkEvent
+from common.tools.grpc.BaseEventCollector import BaseEventCollector
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from context.client.ContextClient import ContextClient
+
+LOGGER = logging.getLogger(__name__)
+
+class BaseEventDispatcher(threading.Thread):
+    def __init__(
+        self, events_queue : queue.PriorityQueue,
+        terminate : Optional[threading.Event] = None
+    ) -> None:
+        super().__init__(daemon=True)
+        self._events_queue = events_queue
+        self._terminate = threading.Event() if terminate is None else terminate
+
+    def stop(self):
+        self._terminate.set()
+
+    def _get_event(self, block : bool = True, timeout : Optional[float] = 0.5) -> Optional[Any]:
+        try:
+            _, event = self._events_queue.get(block=block, timeout=timeout)
+            return event
+        except queue.Empty:
+            return None
+
+    def _get_dispatcher(self, event : Any) -> Optional[Callable]:
+        object_name = str(event.__class__.__name__).lower().replace('event', '')
+        event_type  = EventTypeEnum.Name(event.event.event_type).lower().replace('eventtype_', '')
+
+        method_name = 'dispatch_{:s}_{:s}'.format(object_name, event_type)
+        dispatcher  = getattr(self, method_name, None)
+        if dispatcher is not None: return dispatcher
+
+        method_name = 'dispatch_{:s}'.format(object_name)
+        dispatcher  = getattr(self, method_name, None)
+        if dispatcher is not None: return dispatcher
+
+        method_name = 'dispatch'
+        dispatcher  = getattr(self, method_name, None)
+        if dispatcher is not None: return dispatcher
+
+        return None
+
+    def run(self) -> None:
+        while not self._terminate.is_set():
+            event = self._get_event()
+            if event is None: continue
+
+            dispatcher = self._get_dispatcher(event)
+            if dispatcher is None:
+                MSG = 'No dispatcher available for Event({:s})'
+                LOGGER.warning(MSG.format(grpc_message_to_json_string(event)))
+                continue
+
+            dispatcher(event)
+
+class MyEventDispatcher(BaseEventDispatcher):
+    def dispatch_device_create(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Create: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_device_update(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Update: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_device_remove(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_link(self, link_event : LinkEvent) -> None:
+        MSG = 'Processing Link Create/Update/Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(link_event)))
+
+    def dispatch(self, event : Any) -> None:
+        MSG = 'Processing any other Event: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(event)))
+
+def main() -> None:
+    logging.basicConfig(level=logging.INFO)
+
+    context_client = ContextClient()
+    context_client.connect()
+
+    event_collector = BaseEventCollector()
+    event_collector.install_collector(context_client.GetDeviceEvents,  Empty(), log_events_received=True)
+    event_collector.install_collector(context_client.GetLinkEvents,    Empty(), log_events_received=True)
+    event_collector.install_collector(context_client.GetServiceEvents, Empty(), log_events_received=True)
+    event_collector.start()
+
+    event_dispatcher = MyEventDispatcher(event_collector.get_events_queue())
+    event_dispatcher.start()
+
+    time.sleep(60)
+
+    event_dispatcher.stop()
+    event_collector.stop()
+    context_client.close()
+
+if __name__ == '__main__':
+    main()
diff --git a/src/common/tools/grpc/ExampleEventEngine.py b/src/common/tools/grpc/ExampleEventEngine.py
new file mode 100644
index 0000000000000000000000000000000000000000..f27792497db09467c0225f07d036adc8c5b5ed84
--- /dev/null
+++ b/src/common/tools/grpc/ExampleEventEngine.py
@@ -0,0 +1,101 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, threading, time
+from typing import Optional
+from common.proto.context_pb2 import DeviceEvent, Empty, ServiceEvent
+from common.tools.grpc.BaseEventCollector import BaseEventCollector
+from common.tools.grpc.BaseEventDispatcher import BaseEventDispatcher
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from context.client.ContextClient import ContextClient
+
+LOGGER = logging.getLogger(__name__)
+
+class EventCollector(BaseEventCollector):
+    pass
+
+class EventDispatcher(BaseEventDispatcher):
+    def dispatch_device_create(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Create: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_device_update(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Update: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_device_remove(self, device_event : DeviceEvent) -> None:
+        MSG = 'Processing Device Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(device_event)))
+
+    def dispatch_service_create(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Create: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+    def dispatch_service_update(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Update: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+    def dispatch_service_remove(self, service_event : ServiceEvent) -> None:
+        MSG = 'Processing Service Remove: {:s}'
+        LOGGER.info(MSG.format(grpc_message_to_json_string(service_event)))
+
+class ExampleEventEngine:
+    def __init__(
+        self, terminate : Optional[threading.Event] = None
+    ) -> None:
+        self._terminate = threading.Event() if terminate is None else terminate
+
+        self._context_client = ContextClient()
+        self._event_collector = EventCollector(terminate=self._terminate)
+        self._event_collector.install_collector(
+            self._context_client.GetDeviceEvents, Empty(),
+            log_events_received=True
+        )
+        self._event_collector.install_collector(
+            self._context_client.GetLinkEvents, Empty(),
+            log_events_received=True
+        )
+        self._event_collector.install_collector(
+            self._context_client.GetServiceEvents, Empty(),
+            log_events_received=True
+        )
+
+        self._event_dispatcher = EventDispatcher(
+            self._event_collector.get_events_queue(),
+            terminate=self._terminate
+        )
+
+    def start(self) -> None:
+        self._context_client.connect()
+        self._event_collector.start()
+        self._event_dispatcher.start()
+
+    def stop(self) -> None:
+        self._terminate.set()
+        self._event_dispatcher.stop()
+        self._event_collector.stop()
+        self._context_client.close()
+
+def main() -> None:
+    logging.basicConfig(level=logging.INFO)
+
+    event_engine = ExampleEventEngine()
+    event_engine.start()
+
+    time.sleep(60)
+
+    event_engine.stop()
+
+if __name__ == '__main__':
+    main()
diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py
index ebbf19607a7c591f3414d0a9b276930a6b7b1c00..7546c225e67fd3122ec845b3154606eddb7cd9ff 100644
--- a/src/device/service/DeviceServiceServicerImpl.py
+++ b/src/device/service/DeviceServiceServicerImpl.py
@@ -251,8 +251,15 @@ class DeviceServiceServicerImpl(DeviceServiceServicer):
                 device_id = context_client.SetDevice(device)
                 device = context_client.GetDevice(device_id)
 
-            if request.device_operational_status != DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED:
-                device.device_operational_status = request.device_operational_status
+            ztp_service_host = get_env_var_name(ServiceNameEnum.ZTP, ENVVAR_SUFIX_SERVICE_HOST)
+            environment_variables = set(os.environ.keys())
+            if ztp_service_host in environment_variables:
+                # ZTP component is deployed; accept status updates
+                if request.device_operational_status != DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED:
+                    device.device_operational_status = request.device_operational_status
+            else:
+                # ZTP is not deployed; activated during AddDevice and not modified
+                pass
 
             t4 = time.time()
             # TODO: use of datastores (might be virtual ones) to enable rollbacks
diff --git a/src/device/service/__main__.py b/src/device/service/__main__.py
index e69b7fd3c4518b5cbc3833c457f8803e26b2c8e3..4a75d6284ac700bb1a6d6a388049824c2b301de7 100644
--- a/src/device/service/__main__.py
+++ b/src/device/service/__main__.py
@@ -16,8 +16,10 @@ import logging, signal, sys, threading
 from prometheus_client import start_http_server
 from common.Constants import ServiceNameEnum
 from common.Settings import (
-    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_log_level, get_metrics_port,
-    wait_for_environment_variables)
+    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC,
+    get_env_var_name, get_log_level, get_metrics_port,
+    wait_for_environment_variables
+)
 from .DeviceService import DeviceService
 from .driver_api.DriverFactory import DriverFactory
 from .driver_api.DriverInstanceCache import DriverInstanceCache, preload_drivers
diff --git a/src/e2e_orchestrator/Dockerfile b/src/e2e_orchestrator/Dockerfile
index 1ead42a2fd85316b8f8a45aded98f91d861e6b8d..132d6ba47938e1c74cee08aa3ad41ead6f992f68 100644
--- a/src/e2e_orchestrator/Dockerfile
+++ b/src/e2e_orchestrator/Dockerfile
@@ -77,8 +77,11 @@ RUN pip-compile --quiet --output-file=e2e_orchestrator/requirements.txt e2e_orch
 RUN python3 -m pip install -r e2e_orchestrator/requirements.txt
 
 # Add component files into working directory
-COPY --chown=teraflow:teraflow ./src/context/. context
-COPY --chown=teraflow:teraflow ./src/e2e_orchestrator/. e2e_orchestrator
+COPY src/context/__init__.py context/__init__.py
+COPY src/context/client/. context/client/
+COPY src/service/__init__.py service/__init__.py
+COPY src/service/client/. service/client/
+COPY src/e2e_orchestrator/. e2e_orchestrator/
 
 # Start the service
 ENTRYPOINT ["python", "-m", "e2e_orchestrator.service"]
diff --git a/src/e2e_orchestrator/requirements.in b/src/e2e_orchestrator/requirements.in
index 6553c5a41eb9bcf20b7841d8af1fb84be61a27fc..5732b1bf053301f73a37830e66eb211912d9e200 100644
--- a/src/e2e_orchestrator/requirements.in
+++ b/src/e2e_orchestrator/requirements.in
@@ -12,4 +12,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-networkx
\ No newline at end of file
+networkx
+websockets==12.0
diff --git a/src/e2e_orchestrator/service/E2EOrchestratorServiceServicerImpl.py b/src/e2e_orchestrator/service/E2EOrchestratorServiceServicerImpl.py
index 0ba2eba3390ab13f38ce80affd4faaeba61e1b87..4fc0ea3ba45a85bb3b1dccb18191dc1b4e380404 100644
--- a/src/e2e_orchestrator/service/E2EOrchestratorServiceServicerImpl.py
+++ b/src/e2e_orchestrator/service/E2EOrchestratorServiceServicerImpl.py
@@ -12,33 +12,181 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import logging
-
-import networkx as nx
-import grpc
 import copy
-
-from common.Constants import ServiceNameEnum
-from common.method_wrappers.Decorator import (MetricsPool, MetricTypeEnum, safe_and_metered_rpc_method)
+from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method
 from common.proto.e2eorchestrator_pb2 import E2EOrchestratorRequest, E2EOrchestratorReply
-from common.proto.context_pb2 import Empty, Connection, EndPointId
+from common.proto.context_pb2 import (
+    Empty, Connection, EndPointId, Link, LinkId, TopologyDetails, Topology, Context, Service, ServiceId,
+    ServiceTypeEnum, ServiceStatusEnum)
 from common.proto.e2eorchestrator_pb2_grpc import E2EOrchestratorServiceServicer
+from common.Settings import get_setting
 from context.client.ContextClient import ContextClient
+from service.client.ServiceClient import ServiceClient
 from context.service.database.uuids.EndPoint import endpoint_get_uuid
+from context.service.database.uuids.Device import device_get_uuid
+from common.proto.vnt_manager_pb2 import VNTSubscriptionRequest
+from common.tools.grpc.Tools import grpc_message_to_json_string
+import grpc
+import json
+import logging
+import networkx as nx
+from threading import Thread
+from websockets.sync.client import connect
+from websockets.sync.server import serve
+from common.Constants import DEFAULT_CONTEXT_NAME
 
 
 LOGGER = logging.getLogger(__name__)
+logging.getLogger("websockets").propagate = True
 
 METRICS_POOL = MetricsPool("E2EOrchestrator", "RPC")
 
+
 context_client: ContextClient = ContextClient()
+service_client: ServiceClient = ServiceClient()
+
+EXT_HOST = str(get_setting('WS_IP_HOST'))
+EXT_PORT = str(get_setting('WS_IP_PORT'))
+
+OWN_HOST = str(get_setting('WS_E2E_HOST'))
+OWN_PORT = str(get_setting('WS_E2E_PORT'))
+
+
+ALL_HOSTS = "0.0.0.0"
+
+class SubscriptionServer(Thread):
+    def __init__(self):
+        Thread.__init__(self)
+            
+    def run(self):
+        url = "ws://" + EXT_HOST + ":" + EXT_PORT
+        request = VNTSubscriptionRequest()
+        request.host = OWN_HOST
+        request.port = OWN_PORT
+        try: 
+            LOGGER.debug("Trying to connect to {}".format(url))
+            websocket = connect(url)
+        except Exception as ex:
+            LOGGER.error('Error connecting to {}'.format(url))
+        else:
+            with websocket:
+                LOGGER.debug("Connected to {}".format(url))
+                send = grpc_message_to_json_string(request)
+                websocket.send(send)
+                LOGGER.debug("Sent: {}".format(send))
+                try:
+                    message = websocket.recv()
+                    LOGGER.debug("Received message from WebSocket: {}".format(message))
+                except Exception as ex:
+                    LOGGER.error('Exception receiving from WebSocket: {}'.format(ex))
+
+            self._events_server()
+
+
+    def _events_server(self):
+        all_hosts = "0.0.0.0"
+
+        try:
+            server = serve(self._event_received, all_hosts, int(OWN_PORT))
+        except Exception as ex:
+            LOGGER.error('Error starting server on {}:{}'.format(all_hosts, OWN_PORT))
+            LOGGER.error('Exception!: {}'.format(ex))
+        else:
+            with server:
+                LOGGER.info("Running events server...: {}:{}".format(all_hosts, OWN_PORT))
+                server.serve_forever()
+
+
+    def _event_received(self, connection):
+        LOGGER.info("EVENT received!")
+        for message in connection:
+            message_json = json.loads(message)
+            # LOGGER.info("message_json: {}".format(message_json))
+
+            # Link creation
+            if 'link_id' in message_json:
+                link = Link(**message_json)
+
+                service = Service()
+                service.service_id.service_uuid.uuid = link.link_id.link_uuid.uuid
+                service.service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME
+                service.service_type = ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY
+                service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED
+                service_client.CreateService(service)
+
+                links = context_client.ListLinks(Empty()).links
+                a_device_uuid = device_get_uuid(link.link_endpoint_ids[0].device_id)
+                a_endpoint_uuid = endpoint_get_uuid(link.link_endpoint_ids[0])[2]
+                z_device_uuid = device_get_uuid(link.link_endpoint_ids[1].device_id)
+                z_endpoint_uuid = endpoint_get_uuid(link.link_endpoint_ids[1])[2]
+
+                for _link in links:
+                    for _endpoint_id in _link.link_endpoint_ids:
+                        if _endpoint_id.device_id.device_uuid.uuid == a_device_uuid and \
+                        _endpoint_id.endpoint_uuid.uuid == a_endpoint_uuid:
+                            a_ep_id = _endpoint_id
+                        elif _endpoint_id.device_id.device_uuid.uuid == z_device_uuid and \
+                        _endpoint_id.endpoint_uuid.uuid == z_endpoint_uuid:
+                            z_ep_id = _endpoint_id
+
+                if (not 'a_ep_id' in locals()) or (not 'z_ep_id' in locals()):
+                    error_msg = 'Could not get VNT link endpoints'
+                    LOGGER.error(error_msg)
+                    connection.send(error_msg)
+                    return
+
+                service.service_endpoint_ids.append(copy.deepcopy(a_ep_id))
+                service.service_endpoint_ids.append(copy.deepcopy(z_ep_id))
+
+                # service_client.UpdateService(service)
+                connection.send(grpc_message_to_json_string(link))
+            # Link removal
+            elif 'link_uuid' in message_json:
+                LOGGER.info('REMOVING VIRTUAL LINK')
+                link_id = LinkId(**message_json)
+
+                service_id = ServiceId()
+                service_id.service_uuid.uuid = link_id.link_uuid.uuid
+                service_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME
+                # service_client.DeleteService(service_id)
+                connection.send(grpc_message_to_json_string(link_id))
+                context_client.RemoveLink(link_id)
+            # Topology received
+            else:
+                LOGGER.info('TOPOLOGY')
+                topology_details = TopologyDetails(**message_json)
+
+                context = Context()
+                context.context_id.context_uuid.uuid = topology_details.topology_id.context_id.context_uuid.uuid
+                context_client.SetContext(context)
+
+                topology = Topology()
+                topology.topology_id.context_id.CopyFrom(context.context_id)
+                topology.topology_id.topology_uuid.uuid = topology_details.topology_id.topology_uuid.uuid
+                context_client.SetTopology(topology)
+
+                for device in topology_details.devices:
+                    context_client.SetDevice(device)
+
+                for link in topology_details.links:
+                    context_client.SetLink(link)
+
 
 
 class E2EOrchestratorServiceServicerImpl(E2EOrchestratorServiceServicer):
     def __init__(self):
         LOGGER.debug("Creating Servicer...")
-        LOGGER.debug("Servicer Created")
+        try:
+            LOGGER.debug("Requesting subscription")
+            sub_server = SubscriptionServer()
+            sub_server.start()
+            LOGGER.debug("Servicer Created")
+
+        except Exception as ex:
+            LOGGER.info("Exception!: {}".format(ex))
+
 
+        
     @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
     def Compute(self, request: E2EOrchestratorRequest, context: grpc.ServicerContext) -> E2EOrchestratorReply:
         endpoints_ids = []
diff --git a/src/e2e_orchestrator/service/__main__.py b/src/e2e_orchestrator/service/__main__.py
index 5f20fd72f062127e12fc41352e7213fa320d4a94..b4763627d6de04e49765c047d560cc5626fbc17f 100644
--- a/src/e2e_orchestrator/service/__main__.py
+++ b/src/e2e_orchestrator/service/__main__.py
@@ -43,13 +43,6 @@ def main():
     logging.basicConfig(level=log_level)
     LOGGER = logging.getLogger(__name__)
 
-    wait_for_environment_variables(
-        [
-            get_env_var_name(ServiceNameEnum.E2EORCHESTRATOR, ENVVAR_SUFIX_SERVICE_HOST),
-            get_env_var_name(ServiceNameEnum.E2EORCHESTRATOR, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
-        ]
-    )
-
     signal.signal(signal.SIGINT, signal_handler)
     signal.signal(signal.SIGTERM, signal_handler)
 
diff --git a/src/nbi/Dockerfile b/src/nbi/Dockerfile
index c5fc8d32491d7576e93534c75c264e79c37a36c6..ec1c054858b5f97dcdff36e2ccd4c8039942b51e 100644
--- a/src/nbi/Dockerfile
+++ b/src/nbi/Dockerfile
@@ -16,9 +16,24 @@ FROM python:3.9-slim
 
 # Install dependencies
 RUN apt-get --yes --quiet --quiet update && \
-    apt-get --yes --quiet --quiet install wget g++ git && \
+    apt-get --yes --quiet --quiet install wget g++ git build-essential cmake libpcre2-dev python3-dev python3-cffi && \
     rm -rf /var/lib/apt/lists/*
 
+# Download, build and install libyang. Note that APT package is outdated
+# - Ref: https://github.com/CESNET/libyang
+# - Ref: https://github.com/CESNET/libyang-python/
+RUN mkdir -p /var/libyang
+RUN git clone https://github.com/CESNET/libyang.git /var/libyang
+WORKDIR /var/libyang
+RUN git fetch
+RUN git checkout v2.1.148
+RUN mkdir -p /var/libyang/build
+WORKDIR /var/libyang/build
+RUN cmake -D CMAKE_BUILD_TYPE:String="Release" ..
+RUN make
+RUN make install
+RUN ldconfig
+
 # Set Python to show logs as they occur
 ENV PYTHONUNBUFFERED=0
 
@@ -53,24 +68,6 @@ RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto
 RUN rm *.proto
 RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \;
 
-# Download, build and install libyang. Note that APT package is outdated
-# - Ref: https://github.com/CESNET/libyang
-# - Ref: https://github.com/CESNET/libyang-python/
-RUN apt-get --yes --quiet --quiet update && \
-    apt-get --yes --quiet --quiet install build-essential cmake libpcre2-dev python3-dev python3-cffi && \
-    rm -rf /var/lib/apt/lists/*
-RUN mkdir -p /var/libyang
-RUN git clone https://github.com/CESNET/libyang.git /var/libyang
-WORKDIR /var/libyang
-RUN git fetch
-RUN git checkout v2.1.148
-RUN mkdir -p /var/libyang/build
-WORKDIR /var/libyang/build
-RUN cmake -D CMAKE_BUILD_TYPE:String="Release" ..
-RUN make
-RUN make install
-RUN ldconfig
-
 # Create component sub-folders, get specific Python packages
 RUN mkdir -p /var/teraflow/nbi
 WORKDIR /var/teraflow/nbi
@@ -91,6 +88,8 @@ COPY src/slice/__init__.py slice/__init__.py
 COPY src/slice/client/. slice/client/
 COPY src/qkd_app/__init__.py qkd_app/__init__.py
 COPY src/qkd_app/client/. qkd_app/client/
+COPY src/vnt_manager/__init__.py vnt_manager/__init__.py
+COPY src/vnt_manager/client/. vnt_manager/client/
 RUN mkdir -p /var/teraflow/tests/tools
 COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/
 
diff --git a/src/nbi/requirements.in b/src/nbi/requirements.in
index 4c5460a8e2b3c05d994bbaba4bd2939e629db1e2..f0eec63568d900c1324f74d5c9265b614e2ac7d0 100644
--- a/src/nbi/requirements.in
+++ b/src/nbi/requirements.in
@@ -25,3 +25,4 @@ git+https://github.com/robshakir/pyangbind.git
 pydantic==2.6.3
 requests==2.27.1
 werkzeug==2.3.7
+websockets==12.0
diff --git a/src/nbi/service/NbiServiceServicerImpl.py b/src/nbi/service/NbiServiceServicerImpl.py
index b1d62afb11709d9ee237bc1f840b803674923c00..79d8b8ca0074fe63cde141d41a3e6fde475415ee 100644
--- a/src/nbi/service/NbiServiceServicerImpl.py
+++ b/src/nbi/service/NbiServiceServicerImpl.py
@@ -20,7 +20,7 @@ from common.proto.nbi_pb2_grpc import NbiServiceServicer
 
 LOGGER = logging.getLogger(__name__)
 
-METRICS_POOL = MetricsPool('Compute', 'RPC')
+METRICS_POOL = MetricsPool('NBI', 'RPC')
 
 class NbiServiceServicerImpl(NbiServiceServicer):
     def __init__(self):
diff --git a/src/nbi/service/__main__.py b/src/nbi/service/__main__.py
index ddbf7bb8fdeb688cc03b40499e61c5a34f2b82e5..fb735f8a775e8cce1bc696ed4f148b2ab0ec9dcc 100644
--- a/src/nbi/service/__main__.py
+++ b/src/nbi/service/__main__.py
@@ -31,6 +31,7 @@ from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss
 from .rest_server.nbi_plugins.ietf_acl import register_ietf_acl
 from .rest_server.nbi_plugins.qkd_app import register_qkd_app
 from .rest_server.nbi_plugins.tfs_api import register_tfs_api
+from .context_subscription import register_context_subscription
 
 terminate = threading.Event()
 LOGGER = None
@@ -80,6 +81,8 @@ def main():
     register_tfs_api(rest_server)
     rest_server.start()
 
+    register_context_subscription()
+
     LOGGER.debug('Configured Resources:')
     for resource in rest_server.api.resources:
         LOGGER.debug(' - {:s}'.format(str(resource)))
diff --git a/src/nbi/service/context_subscription/__init__.py b/src/nbi/service/context_subscription/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0d8d7d4229b30d05a1c960d32fb337653be3706
--- /dev/null
+++ b/src/nbi/service/context_subscription/__init__.py
@@ -0,0 +1,64 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 websockets.sync.server import serve
+from common.proto.vnt_manager_pb2 import VNTSubscriptionRequest
+from common.Settings import get_setting
+from context.client.ContextClient import ContextClient
+from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
+from common.tools.object_factory.Topology import json_topology_id
+from common.tools.object_factory.Context import json_context_id
+from common.proto.context_pb2 import ContextId, TopologyId
+import json
+import os
+from vnt_manager.client.VNTManagerClient import VNTManagerClient
+
+JSON_ADMIN_CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_NAME)
+ADMIN_CONTEXT_ID = ContextId(**JSON_ADMIN_CONTEXT_ID)
+ADMIN_TOPOLOGY_ID = TopologyId(**json_topology_id(DEFAULT_TOPOLOGY_NAME, context_id=JSON_ADMIN_CONTEXT_ID))
+
+vnt_manager_client: VNTManagerClient =  VNTManagerClient()
+context_client:     ContextClient =     ContextClient()   
+
+ALL_HOSTS = "0.0.0.0"
+WS_E2E_PORT = str(get_setting('WS_E2E_PORT'))
+
+LOGGER = logging.getLogger(__name__)
+
+
+def register_context_subscription():
+    with serve(subcript_to_vnt_manager, ALL_HOSTS, WS_E2E_PORT, logger=LOGGER) as server:
+        LOGGER.info("Running subscription server...: {}:{}".format(ALL_HOSTS, str(WS_E2E_PORT)))
+        server.serve_forever()
+        LOGGER.info("Exiting subscription server...")
+
+
+def subcript_to_vnt_manager(websocket):
+    for message in websocket:
+        LOGGER.debug("Message received: {}".format(message))
+        message_json = json.loads(message)
+        request = VNTSubscriptionRequest()
+        request.host = message_json['host']
+        request.port = message_json['port']
+        LOGGER.debug("Received gRPC from ws: {}".format(request))
+
+        try:
+            vntm_reply = vnt_manager_client.VNTSubscript(request)
+            LOGGER.debug("Received gRPC from vntm: {}".format(vntm_reply))
+        except Exception as e:
+            LOGGER.error('Could not subscript to VTNManager: {}'.format(e))
+
+        websocket.send(vntm_reply.subscription)
diff --git a/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py b/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py
index f360e318127706b4b4c8fdc4130dfdfc0ba711c0..28f94887a1aa9895a337baa40b3896e3d7e95dc1 100644
--- a/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py
+++ b/src/nbi/service/rest_server/nbi_plugins/tfs_api/Resources.py
@@ -13,27 +13,34 @@
 # limitations under the License.
 
 import json
+import logging
 from flask.json import jsonify
 from flask_restful import Resource, request
 from werkzeug.exceptions import BadRequest
-from common.proto.context_pb2 import Empty
+from common.proto.context_pb2 import Empty, LinkTypeEnum
 from common.tools.grpc.Tools import grpc_message_to_json
 from context.client.ContextClient import ContextClient
 from device.client.DeviceClient import DeviceClient
 from service.client.ServiceClient import ServiceClient
 from slice.client.SliceClient import SliceClient
+from vnt_manager.client.VNTManagerClient import VNTManagerClient
+
 from .Tools import (
     format_grpc_to_json, grpc_connection_id, grpc_context, grpc_context_id, grpc_device,
     grpc_device_id, grpc_link, grpc_link_id, grpc_policy_rule_id,
     grpc_service_id, grpc_service, grpc_slice, grpc_slice_id, grpc_topology, grpc_topology_id
 )
 
+LOGGER = logging.getLogger(__name__)
+
+
 class _Resource(Resource):
     def __init__(self) -> None:
         super().__init__()
         self.context_client = ContextClient()
         self.device_client  = DeviceClient()
         self.service_client = ServiceClient()
+        self.vntmanager_client = VNTManagerClient()
         self.slice_client   = SliceClient()
 
 class ContextIds(_Resource):
@@ -292,9 +299,14 @@ class Link(_Resource):
         return format_grpc_to_json(self.context_client.GetLink(grpc_link_id(link_uuid)))
 
     def put(self, link_uuid : str):
-        link = request.get_json()
-        if link_uuid != link['link_id']['link_uuid']['uuid']:
+        link_json = request.get_json()
+        link = grpc_link(link_json)
+        virtual_types = {LinkTypeEnum.LINKTYPE_VIRTUAL_COPPER, LinkTypeEnum.LINKTYPE_VIRTUAL_OPTICAL}
+        if link_uuid != link.link_id.link_uuid.uuid:
             raise BadRequest('Mismatching link_uuid')
+        elif link.link_type in virtual_types:
+            link = grpc_link(link_json)
+            return format_grpc_to_json(self.vntmanager_client.SetVirtualLink(link))    
         return format_grpc_to_json(self.context_client.SetLink(grpc_link(link)))
 
     def delete(self, link_uuid : str):
diff --git a/src/telemetry/backend/service/TelemetryBackendService.py b/src/telemetry/backend/service/TelemetryBackendService.py
index 79a35d343860d19992518c0e8b29e427e5cbbef4..81ef24481cffc70c6b33bbfbf19d57b062729891 100755
--- a/src/telemetry/backend/service/TelemetryBackendService.py
+++ b/src/telemetry/backend/service/TelemetryBackendService.py
@@ -106,7 +106,7 @@ class TelemetryBackendService(GenericGrpcService):
         Method receives collector request and initiates collecter backend.
         """
         # print("Initiating backend for collector: ", collector_id)
-        LOGGER.info("Initiating backend for collector: ", collector_id)
+        LOGGER.info("Initiating backend for collector: {:s}".format(str(collector_id)))
         start_time = time.time()
         while not stop_event.is_set():
             if int(collector['duration']) != -1 and time.time() - start_time >= collector['duration']:            # condition to terminate backend
@@ -165,9 +165,9 @@ class TelemetryBackendService(GenericGrpcService):
         Args: err (KafkaError): Kafka error object.
               msg (Message): Kafka message object.
         """
-        if err: 
-            LOGGER.debug('Message delivery failed: {:}'.format(err))
+        if err:
+            LOGGER.error('Message delivery failed: {:}'.format(err))
             # print(f'Message delivery failed: {err}')
-        else:
-            LOGGER.info('Message delivered to topic {:}'.format(msg.topic()))
-            # print(f'Message delivered to topic {msg.topic()}')
+        #else:
+        #    LOGGER.debug('Message delivered to topic {:}'.format(msg.topic()))
+        #    # print(f'Message delivered to topic {msg.topic()}')
diff --git a/src/telemetry/frontend/client/TelemetryFrontendClient.py b/src/telemetry/frontend/client/TelemetryFrontendClient.py
index cd36ecd45933ad10758e408cf03c1bf834d27ba6..afcf241530a41f1f4ab1729379a4e5196c25d04f 100644
--- a/src/telemetry/frontend/client/TelemetryFrontendClient.py
+++ b/src/telemetry/frontend/client/TelemetryFrontendClient.py
@@ -29,8 +29,8 @@ RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION,
 
 class TelemetryFrontendClient:
     def __init__(self, host=None, port=None):
-        if not host: host = get_service_host(ServiceNameEnum.TELEMETRYFRONTEND)
-        if not port: port = get_service_port_grpc(ServiceNameEnum.TELEMETRYFRONTEND)
+        if not host: host = get_service_host(ServiceNameEnum.TELEMETRY)
+        if not port: port = get_service_port_grpc(ServiceNameEnum.TELEMETRY)
         self.endpoint = '{:s}:{:s}'.format(str(host), str(port))
         LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint)))
         self.channel = None
diff --git a/src/telemetry/frontend/service/TelemetryFrontendService.py b/src/telemetry/frontend/service/TelemetryFrontendService.py
index abd361aa0082e2de1d1f5fa7e81a336f3091af9a..49def20a1ce3cee1062d1e582fd8ec28308652b7 100644
--- a/src/telemetry/frontend/service/TelemetryFrontendService.py
+++ b/src/telemetry/frontend/service/TelemetryFrontendService.py
@@ -21,7 +21,7 @@ from telemetry.frontend.service.TelemetryFrontendServiceServicerImpl import Tele
 
 class TelemetryFrontendService(GenericGrpcService):
     def __init__(self, cls_name: str = __name__) -> None:
-        port = get_service_port_grpc(ServiceNameEnum.TELEMETRYFRONTEND)
+        port = get_service_port_grpc(ServiceNameEnum.TELEMETRY)
         super().__init__(port, cls_name=cls_name)
         self.telemetry_frontend_servicer = TelemetryFrontendServiceServicerImpl()
 
diff --git a/src/telemetry/frontend/tests/test_frontend.py b/src/telemetry/frontend/tests/test_frontend.py
index c3f8091c83f56fd4a134ec092b1e22723040595d..988d76af0380302cd6351d46eccf6159bf1dc5ab 100644
--- a/src/telemetry/frontend/tests/test_frontend.py
+++ b/src/telemetry/frontend/tests/test_frontend.py
@@ -36,9 +36,9 @@ from telemetry.frontend.service.TelemetryFrontendServiceServicerImpl import Tele
 
 LOCAL_HOST = '127.0.0.1'
 
-TELEMETRY_FRONTEND_PORT = str(get_service_port_grpc(ServiceNameEnum.TELEMETRYFRONTEND))
-os.environ[get_env_var_name(ServiceNameEnum.TELEMETRYFRONTEND, ENVVAR_SUFIX_SERVICE_HOST     )] = str(LOCAL_HOST)
-os.environ[get_env_var_name(ServiceNameEnum.TELEMETRYFRONTEND, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(TELEMETRY_FRONTEND_PORT)
+TELEMETRY_FRONTEND_PORT = str(get_service_port_grpc(ServiceNameEnum.TELEMETRY))
+os.environ[get_env_var_name(ServiceNameEnum.TELEMETRY, ENVVAR_SUFIX_SERVICE_HOST     )] = str(LOCAL_HOST)
+os.environ[get_env_var_name(ServiceNameEnum.TELEMETRY, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(TELEMETRY_FRONTEND_PORT)
 
 LOGGER = logging.getLogger(__name__)
 
diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml
index ed4bad5f60117a061d3879cd0b2590b794a06eac..de369a3be1e814f77e1ab20575248e5cb69c3c4a 100644
--- a/src/tests/.gitlab-ci.yml
+++ b/src/tests/.gitlab-ci.yml
@@ -14,10 +14,11 @@
 
 # include the individual .gitlab-ci.yml of each end-to-end integration test
 include:
-  - local: '/src/tests/eucnc24/.gitlab-ci.yml'
   #- local: '/src/tests/ofc22/.gitlab-ci.yml'
   #- local: '/src/tests/oeccpsc22/.gitlab-ci.yml'
   #- 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/eucnc24/.gitlab-ci.yml'
+  #- local: '/src/tests/ecoc24/.gitlab-ci.yml'
diff --git a/src/tests/ecoc24/__init__.py b/src/tests/ecoc24/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3
--- /dev/null
+++ b/src/tests/ecoc24/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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/ecoc24/deploy_e2e.sh b/src/tests/ecoc24/deploy_e2e.sh
new file mode 100755
index 0000000000000000000000000000000000000000..23ff8b7f037b8f2497e6ef851c3aa9f301772e1a
--- /dev/null
+++ b/src/tests/ecoc24/deploy_e2e.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# Delete old namespaces
+kubectl delete namespace tfs-e2e
+
+# Delete secondary ingress controllers
+kubectl delete -f src/tests/ecoc24/nginx-ingress-controller-e2e.yaml
+
+# Create secondary ingress controllers
+kubectl apply -f src/tests/ecoc24/nginx-ingress-controller-e2e.yaml
+
+# Deploy TFS for E2E
+source src/tests/ecoc24/deploy_specs_e2e.sh
+./deploy/all.sh
+
+#Configure Subscription WS
+./src/tests/ecoc24/deploy/subscription_ws_e2e.sh
+
+mv tfs_runtime_env_vars.sh tfs_runtime_env_vars_e2e.sh
diff --git a/src/tests/ecoc24/deploy_ip.sh b/src/tests/ecoc24/deploy_ip.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a6c5e82557d02c89f67bbbf0ee0a6f88650ba9d1
--- /dev/null
+++ b/src/tests/ecoc24/deploy_ip.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# Delete old namespaces
+kubectl delete namespace tfs-ip
+
+# Delete secondary ingress controllers
+kubectl delete -f src/tests/ecoc24/nginx-ingress-controller-ip.yaml
+
+# Create secondary ingress controllers
+kubectl apply -f src/tests/ecoc24/nginx-ingress-controller-ip.yaml
+
+# Deploy TFS for IP
+source src/tests/ecoc24/deploy_specs_ip.sh
+./deploy/all.sh
+
+#Configure Subscription WS
+./src/tests/ecoc24/subscription_ws_ip.sh
+
+mv tfs_runtime_env_vars.sh tfs_runtime_env_vars_ip.sh
diff --git a/src/tests/ecoc24/deploy_opt.sh b/src/tests/ecoc24/deploy_opt.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3a9523768ec21c2e177a3567a51cdc94f4db992b
--- /dev/null
+++ b/src/tests/ecoc24/deploy_opt.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# Delete old namespaces
+kubectl delete namespace tfs-opt
+
+# Delete secondary ingress controllers
+kubectl delete -f src/tests/ecoc24/nginx-ingress-controller-opt.yaml
+
+# Create secondary ingress controllers
+kubectl apply -f src/tests/ecoc24/nginx-ingress-controller-opt.yaml
+
+# Deploy TFS for OPT
+source src/tests/ecoc24/deploy_specs_opt.sh
+./deploy/all.sh
+mv tfs_runtime_env_vars.sh tfs_runtime_env_vars_opt.sh
diff --git a/src/tests/ecoc24/deploy_specs_e2e.sh b/src/tests/ecoc24/deploy_specs_e2e.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e1e358cfcebe6d079f175e10a67bd097bf46dd7e
--- /dev/null
+++ b/src/tests/ecoc24/deploy_specs_e2e.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# ----- TeraFlowSDN ------------------------------------------------------------
+
+# Set the URL of the internal MicroK8s Docker registry where the images will be uploaded to.
+export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/"
+
+# Set the list of components, separated by spaces, you want to build images for, and deploy.
+export TFS_COMPONENTS="context device pathcomp service slice nbi webui"
+
+# Uncomment to activate Monitoring (old)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring"
+
+# Uncomment to activate Monitoring Framework (new)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation"
+
+# Uncomment to activate QoS Profiles
+#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile"
+
+# Uncomment to activate BGP-LS Speaker
+#export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker"
+
+# Uncomment to activate Optical Controller
+#   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"
+
+# Uncomment to activate Policy Manager
+#export TFS_COMPONENTS="${TFS_COMPONENTS} policy"
+
+# Uncomment to activate Optical CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} dbscanserving opticalattackmitigator opticalattackdetector opticalattackmanager"
+
+# Uncomment to activate L3 CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} l3_attackmitigator l3_centralizedattackdetector"
+
+# Uncomment to activate TE
+#export TFS_COMPONENTS="${TFS_COMPONENTS} te"
+
+# Uncomment to activate Forecaster
+#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster"
+
+# Uncomment to activate E2E Orchestrator
+export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"
+
+# Uncomment to activate VNT Manager
+# export TFS_COMPONENTS="${TFS_COMPONENTS} vnt_manager"
+
+# Uncomment to activate DLT and Interdomain
+#export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt"
+#if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then
+#    export KEY_DIRECTORY_PATH="src/dlt/gateway/keys/priv_sk"
+#    export CERT_DIRECTORY_PATH="src/dlt/gateway/keys/cert.pem"
+#    export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt"
+#fi
+
+# Uncomment to activate QKD App
+#   To manage QKD Apps, "service" requires "qkd_app" to be deployed
+#   before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+#   "qkd_app" 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} qkd_app service ${AFTER}"
+#fi
+
+# Uncomment to activate Load Generator
+#export TFS_COMPONENTS="${TFS_COMPONENTS} load_generator"
+
+
+# Set the tag you want to use for your images.
+export TFS_IMAGE_TAG="dev"
+
+# Set the name of the Kubernetes namespace to deploy TFS to.
+export TFS_K8S_NAMESPACE="tfs-e2e"
+
+# Set additional manifest files to be applied after the deployment
+export TFS_EXTRA_MANIFESTS="src/tests/ecoc24/tfs-ingress-e2e.yaml"
+
+# Uncomment to monitor performance of components
+#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"
+
+# Set the new Grafana admin password
+export TFS_GRAFANA_PASSWORD="admin123+"
+
+# Disable skip-build flag to rebuild the Docker images.
+export TFS_SKIP_BUILD=""
+
+
+# ----- CockroachDB ------------------------------------------------------------
+
+# Set the namespace where CockroackDB will be deployed.
+export CRDB_NAMESPACE="crdb"
+
+# Set the external port CockroackDB Postgre SQL interface will be exposed to.
+export CRDB_EXT_PORT_SQL="26257"
+
+# Set the external port CockroackDB HTTP Mgmt GUI interface will be exposed to.
+export CRDB_EXT_PORT_HTTP="8081"
+
+# Set the database username to be used by Context.
+export CRDB_USERNAME="tfs"
+
+# Set the database user's password to be used by Context.
+export CRDB_PASSWORD="tfs123"
+
+# Set the database name to be used by Context.
+export CRDB_DATABASE="tfs_e2e"
+
+# Set CockroachDB installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/crdb.sh for additional details
+export CRDB_DEPLOY_MODE="single"
+
+# Disable flag for dropping database, if it exists.
+export CRDB_DROP_DATABASE_IF_EXISTS="YES"
+
+# Disable flag for re-deploying CockroachDB from scratch.
+export CRDB_REDEPLOY=""
+
+
+# ----- NATS -------------------------------------------------------------------
+
+# Set the namespace where NATS will be deployed.
+export NATS_NAMESPACE="nats-e2e"
+
+# Set the external port NATS Client interface will be exposed to.
+export NATS_EXT_PORT_CLIENT="4223"
+
+# Set the external port NATS HTTP Mgmt GUI interface will be exposed to.
+export NATS_EXT_PORT_HTTP="8223"
+
+# Set NATS installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/nats.sh for additional details
+export NATS_DEPLOY_MODE="single"
+
+# Disable flag for re-deploying NATS from scratch.
+export NATS_REDEPLOY=""
+
+
+# ----- QuestDB ----------------------------------------------------------------
+
+# Set the namespace where QuestDB will be deployed.
+export QDB_NAMESPACE="qdb-e2e"
+
+# Set the external port QuestDB Postgre SQL interface will be exposed to.
+export QDB_EXT_PORT_SQL="8813"
+
+# Set the external port QuestDB Influx Line Protocol interface will be exposed to.
+export QDB_EXT_PORT_ILP="9011"
+
+# Set the external port QuestDB HTTP Mgmt GUI interface will be exposed to.
+export QDB_EXT_PORT_HTTP="9001"
+
+# Set the database username to be used for QuestDB.
+export QDB_USERNAME="admin"
+
+# Set the database user's password to be used for QuestDB.
+export QDB_PASSWORD="quest"
+
+# Set the table name to be used by Monitoring for KPIs.
+export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis"
+
+# Set the table name to be used by Slice for plotting groups.
+export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups"
+
+# Disable flag for dropping tables if they exist.
+export QDB_DROP_TABLES_IF_EXIST="YES"
+
+# Disable flag for re-deploying QuestDB from scratch.
+export QDB_REDEPLOY=""
+
+
+# ----- K8s Observability ------------------------------------------------------
+
+# Set the external port Prometheus Mgmt HTTP GUI interface will be exposed to.
+export PROM_EXT_PORT_HTTP="9090"
+
+# Set the external port Grafana HTTP Dashboards will be exposed to.
+export GRAF_EXT_PORT_HTTP="3000"
+
+
+# ----- Apache Kafka -----------------------------------------------------------
+
+# Set the namespace where Apache Kafka will be deployed.
+export KFK_NAMESPACE="kafka"
+
+# Set the port Apache Kafka server will be exposed to.
+export KFK_SERVER_PORT="9092"
+
+# Set the flag to YES for redeploying of Apache Kafka
+export KFK_REDEPLOY=""
diff --git a/src/tests/ecoc24/deploy_specs_ip.sh b/src/tests/ecoc24/deploy_specs_ip.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7542a0fb54bacf0273a5b38ca23ca67fae352c0c
--- /dev/null
+++ b/src/tests/ecoc24/deploy_specs_ip.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# ----- TeraFlowSDN ------------------------------------------------------------
+
+# Set the URL of the internal MicroK8s Docker registry where the images will be uploaded to.
+export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/"
+
+# Set the list of components, separated by spaces, you want to build images for, and deploy.
+export TFS_COMPONENTS="context device pathcomp service slice nbi webui"
+
+# Uncomment to activate Monitoring (old)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring"
+
+# Uncomment to activate Monitoring Framework (new)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation"
+
+# Uncomment to activate QoS Profiles
+#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile"
+
+# Uncomment to activate BGP-LS Speaker
+#export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker"
+
+# Uncomment to activate Optical Controller
+#   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"
+
+# Uncomment to activate Policy Manager
+#export TFS_COMPONENTS="${TFS_COMPONENTS} policy"
+
+# Uncomment to activate Optical CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} dbscanserving opticalattackmitigator opticalattackdetector opticalattackmanager"
+
+# Uncomment to activate L3 CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} l3_attackmitigator l3_centralizedattackdetector"
+
+# Uncomment to activate TE
+#export TFS_COMPONENTS="${TFS_COMPONENTS} te"
+
+# Uncomment to activate Forecaster
+#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster"
+
+# Uncomment to activate E2E Orchestrator
+# export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"
+
+# Uncomment to activate VNT Manager
+export TFS_COMPONENTS="${TFS_COMPONENTS} vnt_manager"
+
+# Uncomment to activate DLT and Interdomain
+#export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt"
+#if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then
+#    export KEY_DIRECTORY_PATH="src/dlt/gateway/keys/priv_sk"
+#    export CERT_DIRECTORY_PATH="src/dlt/gateway/keys/cert.pem"
+#    export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt"
+#fi
+
+# Uncomment to activate QKD App
+#   To manage QKD Apps, "service" requires "qkd_app" to be deployed
+#   before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+#   "qkd_app" 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} qkd_app service ${AFTER}"
+#fi
+
+# Uncomment to activate Load Generator
+#export TFS_COMPONENTS="${TFS_COMPONENTS} load_generator"
+
+
+# Set the tag you want to use for your images.
+export TFS_IMAGE_TAG="dev"
+
+# Set the name of the Kubernetes namespace to deploy TFS to.
+export TFS_K8S_NAMESPACE="tfs-ip"
+
+# Set additional manifest files to be applied after the deployment
+export TFS_EXTRA_MANIFESTS="src/tests/ecoc24/tfs-ingress-ip.yaml"
+
+# Uncomment to monitor performance of components
+#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"
+
+# Set the new Grafana admin password
+export TFS_GRAFANA_PASSWORD="admin123+"
+
+# Disable skip-build flag to rebuild the Docker images.
+export TFS_SKIP_BUILD=""
+
+
+# ----- CockroachDB ------------------------------------------------------------
+
+# Set the namespace where CockroackDB will be deployed.
+export CRDB_NAMESPACE="crdb"
+
+# Set the external port CockroackDB Postgre SQL interface will be exposed to.
+export CRDB_EXT_PORT_SQL="26257"
+
+# Set the external port CockroackDB HTTP Mgmt GUI interface will be exposed to.
+export CRDB_EXT_PORT_HTTP="8081"
+
+# Set the database username to be used by Context.
+export CRDB_USERNAME="tfs"
+
+# Set the database user's password to be used by Context.
+export CRDB_PASSWORD="tfs123"
+
+# Set the database name to be used by Context.
+export CRDB_DATABASE="tfs_ip"
+
+# Set CockroachDB installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/crdb.sh for additional details
+export CRDB_DEPLOY_MODE="single"
+
+# Disable flag for dropping database, if it exists.
+export CRDB_DROP_DATABASE_IF_EXISTS="YES"
+
+# Disable flag for re-deploying CockroachDB from scratch.
+export CRDB_REDEPLOY=""
+
+
+# ----- NATS -------------------------------------------------------------------
+
+# Set the namespace where NATS will be deployed.
+export NATS_NAMESPACE="nats-ip"
+
+# Set the external port NATS Client interface will be exposed to.
+export NATS_EXT_PORT_CLIENT="4224"
+
+# Set the external port NATS HTTP Mgmt GUI interface will be exposed to.
+export NATS_EXT_PORT_HTTP="8224"
+
+# Set NATS installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/nats.sh for additional details
+export NATS_DEPLOY_MODE="single"
+
+# Disable flag for re-deploying NATS from scratch.
+export NATS_REDEPLOY=""
+
+
+# ----- QuestDB ----------------------------------------------------------------
+
+# Set the namespace where QuestDB will be deployed.
+export QDB_NAMESPACE="qdb-ip"
+
+# Set the external port QuestDB Postgre SQL interface will be exposed to.
+export QDB_EXT_PORT_SQL="8814"
+
+# Set the external port QuestDB Influx Line Protocol interface will be exposed to.
+export QDB_EXT_PORT_ILP="9012"
+
+# Set the external port QuestDB HTTP Mgmt GUI interface will be exposed to.
+export QDB_EXT_PORT_HTTP="9002"
+
+# Set the database username to be used for QuestDB.
+export QDB_USERNAME="admin"
+
+# Set the database user's password to be used for QuestDB.
+export QDB_PASSWORD="quest"
+
+# Set the table name to be used by Monitoring for KPIs.
+export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis"
+
+# Set the table name to be used by Slice for plotting groups.
+export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups"
+
+# Disable flag for dropping tables if they exist.
+export QDB_DROP_TABLES_IF_EXIST="YES"
+
+# Disable flag for re-deploying QuestDB from scratch.
+export QDB_REDEPLOY=""
+
+
+# ----- K8s Observability ------------------------------------------------------
+
+# Set the external port Prometheus Mgmt HTTP GUI interface will be exposed to.
+export PROM_EXT_PORT_HTTP="9090"
+
+# Set the external port Grafana HTTP Dashboards will be exposed to.
+export GRAF_EXT_PORT_HTTP="3000"
+
+
+# ----- Apache Kafka -----------------------------------------------------------
+
+# Set the namespace where Apache Kafka will be deployed.
+export KFK_NAMESPACE="kafka"
+
+# Set the port Apache Kafka server will be exposed to.
+export KFK_SERVER_PORT="9092"
+
+# Set the flag to YES for redeploying of Apache Kafka
+export KFK_REDEPLOY=""
diff --git a/src/tests/ecoc24/deploy_specs_opt.sh b/src/tests/ecoc24/deploy_specs_opt.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5d258e60fdb632c692bd1a0ba6ab911e98ae7dc4
--- /dev/null
+++ b/src/tests/ecoc24/deploy_specs_opt.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+# ----- TeraFlowSDN ------------------------------------------------------------
+
+# Set the URL of the internal MicroK8s Docker registry where the images will be uploaded to.
+export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/"
+
+# Set the list of components, separated by spaces, you want to build images for, and deploy.
+export TFS_COMPONENTS="context device pathcomp service slice nbi webui"
+
+# Uncomment to activate Monitoring (old)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring"
+
+# Uncomment to activate Monitoring Framework (new)
+#export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation"
+
+# Uncomment to activate QoS Profiles
+#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile"
+
+# Uncomment to activate BGP-LS Speaker
+#export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker"
+
+# Uncomment to activate Optical Controller
+#   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"
+
+# Uncomment to activate Policy Manager
+#export TFS_COMPONENTS="${TFS_COMPONENTS} policy"
+
+# Uncomment to activate Optical CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} dbscanserving opticalattackmitigator opticalattackdetector opticalattackmanager"
+
+# Uncomment to activate L3 CyberSecurity
+#export TFS_COMPONENTS="${TFS_COMPONENTS} l3_attackmitigator l3_centralizedattackdetector"
+
+# Uncomment to activate TE
+#export TFS_COMPONENTS="${TFS_COMPONENTS} te"
+
+# Uncomment to activate Forecaster
+#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster"
+
+# Uncomment to activate E2E Orchestrator
+# export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"
+
+# Uncomment to activate VNT Manager
+# export TFS_COMPONENTS="${TFS_COMPONENTS} vnt_manager"
+
+# Uncomment to activate DLT and Interdomain
+#export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt"
+#if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then
+#    export KEY_DIRECTORY_PATH="src/dlt/gateway/keys/priv_sk"
+#    export CERT_DIRECTORY_PATH="src/dlt/gateway/keys/cert.pem"
+#    export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt"
+#fi
+
+# Uncomment to activate QKD App
+#   To manage QKD Apps, "service" requires "qkd_app" to be deployed
+#   before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+#   "qkd_app" 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} qkd_app service ${AFTER}"
+#fi
+
+# Uncomment to activate Load Generator
+#export TFS_COMPONENTS="${TFS_COMPONENTS} load_generator"
+
+
+# Set the tag you want to use for your images.
+export TFS_IMAGE_TAG="dev"
+
+# Set the name of the Kubernetes namespace to deploy TFS to.
+export TFS_K8S_NAMESPACE="tfs-opt"
+
+# Set additional manifest files to be applied after the deployment
+export TFS_EXTRA_MANIFESTS="src/tests/ecoc24/tfs-ingress-opt.yaml"
+
+# Uncomment to monitor performance of components
+#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"
+
+# Set the new Grafana admin password
+export TFS_GRAFANA_PASSWORD="admin123+"
+
+# Disable skip-build flag to rebuild the Docker images.
+export TFS_SKIP_BUILD=""
+
+
+# ----- CockroachDB ------------------------------------------------------------
+
+# Set the namespace where CockroackDB will be deployed.
+export CRDB_NAMESPACE="crdb"
+
+# Set the external port CockroackDB Postgre SQL interface will be exposed to.
+export CRDB_EXT_PORT_SQL="26257"
+
+# Set the external port CockroackDB HTTP Mgmt GUI interface will be exposed to.
+export CRDB_EXT_PORT_HTTP="8081"
+
+# Set the database username to be used by Context.
+export CRDB_USERNAME="tfs"
+
+# Set the database user's password to be used by Context.
+export CRDB_PASSWORD="tfs123"
+
+# Set the database name to be used by Context.
+export CRDB_DATABASE="tfs_ip"
+
+# Set CockroachDB installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/crdb.sh for additional details
+export CRDB_DEPLOY_MODE="single"
+
+# Disable flag for dropping database, if it exists.
+export CRDB_DROP_DATABASE_IF_EXISTS="YES"
+
+# Disable flag for re-deploying CockroachDB from scratch.
+export CRDB_REDEPLOY=""
+
+
+# ----- NATS -------------------------------------------------------------------
+
+# Set the namespace where NATS will be deployed.
+export NATS_NAMESPACE="nats-opt"
+
+# Set the external port NATS Client interface will be exposed to.
+export NATS_EXT_PORT_CLIENT="4224"
+
+# Set the external port NATS HTTP Mgmt GUI interface will be exposed to.
+export NATS_EXT_PORT_HTTP="8224"
+
+# Set NATS installation mode to 'single'. This option is convenient for development and testing.
+# See ./deploy/all.sh or ./deploy/nats.sh for additional details
+export NATS_DEPLOY_MODE="single"
+
+# Disable flag for re-deploying NATS from scratch.
+export NATS_REDEPLOY=""
+
+
+# ----- QuestDB ----------------------------------------------------------------
+
+# Set the namespace where QuestDB will be deployed.
+export QDB_NAMESPACE="qdb-opt"
+
+# Set the external port QuestDB Postgre SQL interface will be exposed to.
+export QDB_EXT_PORT_SQL="8814"
+
+# Set the external port QuestDB Influx Line Protocol interface will be exposed to.
+export QDB_EXT_PORT_ILP="9012"
+
+# Set the external port QuestDB HTTP Mgmt GUI interface will be exposed to.
+export QDB_EXT_PORT_HTTP="9002"
+
+# Set the database username to be used for QuestDB.
+export QDB_USERNAME="admin"
+
+# Set the database user's password to be used for QuestDB.
+export QDB_PASSWORD="quest"
+
+# Set the table name to be used by Monitoring for KPIs.
+export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis"
+
+# Set the table name to be used by Slice for plotting groups.
+export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups"
+
+# Disable flag for dropping tables if they exist.
+export QDB_DROP_TABLES_IF_EXIST="YES"
+
+# Disable flag for re-deploying QuestDB from scratch.
+export QDB_REDEPLOY=""
+
+
+# ----- K8s Observability ------------------------------------------------------
+
+# Set the external port Prometheus Mgmt HTTP GUI interface will be exposed to.
+export PROM_EXT_PORT_HTTP="9090"
+
+# Set the external port Grafana HTTP Dashboards will be exposed to.
+export GRAF_EXT_PORT_HTTP="3000"
+
+
+# ----- Apache Kafka -----------------------------------------------------------
+
+# Set the namespace where Apache Kafka will be deployed.
+export KFK_NAMESPACE="kafka"
+
+# Set the port Apache Kafka server will be exposed to.
+export KFK_SERVER_PORT="9092"
+
+# Set the flag to YES for redeploying of Apache Kafka
+export KFK_REDEPLOY=""
diff --git a/src/tests/ecoc24/descriptors/emulated/dc-2-dc-service.json b/src/tests/ecoc24/descriptors/emulated/dc-2-dc-service.json
new file mode 100644
index 0000000000000000000000000000000000000000..44c80ad46f727cbe9af7fd78f73184e756d32a92
--- /dev/null
+++ b/src/tests/ecoc24/descriptors/emulated/dc-2-dc-service.json
@@ -0,0 +1,48 @@
+{
+    "services": [
+        {
+            "service_id": {
+                "context_id": {
+                    "context_uuid": {
+                        "uuid": "admin"
+                        }
+                    },
+                "service_uuid": {
+                    "uuid": "dc-2-dc-svc"
+                }
+            },
+            "service_type": 2,
+            "service_status": {"service_status": 1},
+            "service_endpoint_ids": [
+                {"device_id":{"device_uuid":{"uuid":"DC1"}},"endpoint_uuid":{"uuid":"int"}},
+                {"device_id":{"device_uuid":{"uuid":"DC2"}},"endpoint_uuid":{"uuid":"int"}}
+            ],
+            "service_constraints": [
+                {"sla_capacity": {"capacity_gbps": 10.0}},
+                {"sla_latency": {"e2e_latency_ms": 15.2}}
+            ],
+            "service_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "/settings", "resource_value": {
+                    "address_families": ["IPV4"], "bgp_as": 65000, "bgp_route_target": "65000:123",
+                    "mtu": 1512, "vlan_id": 300
+                }}},
+                {"action": 1, "custom": {"resource_key": "/device[PE1]/endpoint[1/1]/settings", "resource_value": {
+                    "route_distinguisher": "65000:123", "router_id": "10.0.0.1",
+                    "address_ip": "3.3.1.1", "address_prefix": 24, "sub_interface_index": 1, "vlan_id": 300
+                }}},
+                {"action": 1, "custom": {"resource_key": "/device[PE2]/endpoint[1/1]/settings", "resource_value": {
+                    "route_distinguisher": "65000:123", "router_id": "10.0.0.2",
+                    "address_ip": "3.3.2.1", "address_prefix": 24, "sub_interface_index": 1, "vlan_id": 300
+                }}},
+                {"action": 1, "custom": {"resource_key": "/device[PE3]/endpoint[1/1]/settings", "resource_value": {
+                    "route_distinguisher": "65000:123", "router_id": "10.0.0.3",
+                    "address_ip": "3.3.3.1", "address_prefix": 24, "sub_interface_index": 1, "vlan_id": 300
+                }}},
+                {"action": 1, "custom": {"resource_key": "/device[PE4]/endpoint[1/1]/settings", "resource_value": {
+                    "route_distinguisher": "65000:123", "router_id": "10.0.0.4",
+                    "address_ip": "3.3.4.1", "address_prefix": 24, "sub_interface_index": 1, "vlan_id": 300
+                }}}
+            ]}
+        }
+    ]
+}
diff --git a/src/tests/ecoc24/descriptors/emulated/descriptor_ip.json b/src/tests/ecoc24/descriptors/emulated/descriptor_ip.json
new file mode 100644
index 0000000000000000000000000000000000000000..516a8bdebba9b8a7dcdc0a245d52dd17b65cce07
--- /dev/null
+++ b/src/tests/ecoc24/descriptors/emulated/descriptor_ip.json
@@ -0,0 +1,34 @@
+{
+    "contexts": [
+        {"context_id": {"context_uuid": {"uuid": "admin"}}}
+    ],
+    "topologies": [
+        {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}}
+    ],
+    "devices": [
+        {
+            "device_id": {"device_uuid": {"uuid": "IP1"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_endpoints": [], "device_operational_status": 0, "device_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+                {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP1"},
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP2"},
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP3"}
+                ]}}}
+            ]}
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "IP2"}}, "device_type": "emu-packet-router", "device_drivers": [0],
+            "device_endpoints": [], "device_operational_status": 0, "device_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}},
+                {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP1"},
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP2"},
+                    {"sample_types": [], "type": "copper/internal", "uuid": "CTP3"}
+                ]}}}
+            ]}
+        }
+    ]
+}
diff --git a/src/tests/ecoc24/descriptors/emulated/link_mapping.json b/src/tests/ecoc24/descriptors/emulated/link_mapping.json
new file mode 100644
index 0000000000000000000000000000000000000000..9ac5a25994c867bb5387e00363e7b3e0908e030d
--- /dev/null
+++ b/src/tests/ecoc24/descriptors/emulated/link_mapping.json
@@ -0,0 +1,34 @@
+{
+    "contexts": [
+        {"context_id": {"context_uuid": {"uuid": "admin"}}}
+    ],
+    "topologies": [
+        {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}}
+    ],
+    "links": [
+        {"link_id": {"link_uuid": {"uuid": "IP1_CTP1-MGON1_OTP1"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP1"}}, "endpoint_uuid": {"uuid": "CTP1"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON1"}}, "endpoint_uuid": {"uuid": "OTP1"}}
+        ]},
+            {"link_id": {"link_uuid": {"uuid": "IP1_CTP2-MGON1_OTP2"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP1"}}, "endpoint_uuid": {"uuid": "CTP2"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON1"}}, "endpoint_uuid": {"uuid": "OTP2"}}
+        ]},
+        {"link_id": {"link_uuid": {"uuid": "IP1CTP3-MGON1_OTP3"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP1"}}, "endpoint_uuid": {"uuid": "CTP3"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON1"}}, "endpoint_uuid": {"uuid": "OTP3"}}
+        ]},            
+        {"link_id": {"link_uuid": {"uuid": "IP2_CTP1-MGON3_OTP1"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP2"}}, "endpoint_uuid": {"uuid": "CTP1"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON3"}}, "endpoint_uuid": {"uuid": "OTP1"}}
+        ]},
+            {"link_id": {"link_uuid": {"uuid": "IP2_CTP2-MGON3_OTP2"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP2"}}, "endpoint_uuid": {"uuid": "CTP2"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON3"}}, "endpoint_uuid": {"uuid": "OTP2"}}
+        ]},
+        {"link_id": {"link_uuid": {"uuid": "IP2_CTP3-MGON3_OTP3"}}, "link_endpoint_ids": [
+            {"device_id": {"device_uuid": {"uuid": "IP2"}}, "endpoint_uuid": {"uuid": "CTP3"}},
+            {"device_id": {"device_uuid": {"uuid": "MG-ON3"}}, "endpoint_uuid": {"uuid": "OTP3"}}
+        ]}
+    ]
+}
diff --git a/src/tests/ecoc24/dump_logs.sh b/src/tests/ecoc24/dump_logs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bb9a35d126a94812d4157d2ba5b4725acbde4caf
--- /dev/null
+++ b/src/tests/ecoc24/dump_logs.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+rm -rf tmp/exec
+
+echo "Collecting logs for E2E..."
+mkdir -p tmp/exec/e2e
+kubectl --namespace tfs-e2e logs deployments/contextservice server > tmp/exec/e2e/context.log
+kubectl --namespace tfs-e2e logs deployments/deviceservice server > tmp/exec/e2e/device.log
+kubectl --namespace tfs-e2e logs deployments/serviceservice server > tmp/exec/e2e/service.log
+kubectl --namespace tfs-e2e logs deployments/pathcompservice frontend > tmp/exec/e2e/pathcomp-frontend.log
+kubectl --namespace tfs-e2e logs deployments/pathcompservice backend > tmp/exec/e2e/pathcomp-backend.log
+kubectl --namespace tfs-e2e logs deployments/sliceservice server > tmp/exec/e2e/slice.log
+printf "\n"
+
+echo "Collecting logs for IP..."
+mkdir -p tmp/exec/ip
+kubectl --namespace tfs-ip logs deployments/contextservice server > tmp/exec/ip/context.log
+kubectl --namespace tfs-ip logs deployments/deviceservice server > tmp/exec/ip/device.log
+kubectl --namespace tfs-ip logs deployments/serviceservice server > tmp/exec/ip/service.log
+kubectl --namespace tfs-ip logs deployments/pathcompservice frontend > tmp/exec/ip/pathcomp-frontend.log
+kubectl --namespace tfs-ip logs deployments/pathcompservice backend > tmp/exec/ip/pathcomp-backend.log
+kubectl --namespace tfs-ip logs deployments/sliceservice server > tmp/exec/ip/slice.log
+printf "\n"
+
+echo "Done!"
diff --git a/src/tests/ecoc24/nginx-ingress-controller-e2e.yaml b/src/tests/ecoc24/nginx-ingress-controller-e2e.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b5f2447aab2ad557e1dc56cdb86a61962494f67d
--- /dev/null
+++ b/src/tests/ecoc24/nginx-ingress-controller-e2e.yaml
@@ -0,0 +1,138 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-load-balancer-microk8s-conf-e2e
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-udp-microk8s-conf-e2e
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-tcp-microk8s-conf-e2e
+  namespace: ingress
+---
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: tfs-ingress-class-e2e
+  annotations:
+    ingressclass.kubernetes.io/is-default-class: "false"
+spec:
+  controller: tfs.etsi.org/controller-class-e2e
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: nginx-ingress-microk8s-controller-e2e
+  namespace: ingress
+  labels:
+    microk8s-application: nginx-ingress-microk8s-e2e
+spec:
+  selector:
+    matchLabels:
+      name: nginx-ingress-microk8s-e2e
+  updateStrategy:
+    rollingUpdate:
+      maxSurge: 0
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        name: nginx-ingress-microk8s-e2e
+    spec:
+      terminationGracePeriodSeconds: 60
+      restartPolicy: Always
+      serviceAccountName: nginx-ingress-microk8s-serviceaccount
+      containers:
+      - image: k8s.gcr.io/ingress-nginx/controller:v1.2.0
+        imagePullPolicy: IfNotPresent
+        name: nginx-ingress-microk8s
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command:
+                - /wait-shutdown
+        securityContext:
+          capabilities:
+            add:
+            - NET_BIND_SERVICE
+            drop:
+            - ALL
+          runAsUser: 101 # www-data
+        env:
+          - name: POD_NAME
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.name
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.namespace
+        ports:
+        - name: http
+          containerPort: 80
+          hostPort: 8001
+          protocol: TCP
+        - name: https
+          containerPort: 443
+          hostPort: 4431
+          protocol: TCP
+        - name: health
+          containerPort: 10254
+          hostPort: 12541
+          protocol: TCP
+        - name: ws
+          containerPort: 8761
+          hostPort: 8761
+          protocol: TCP
+        args:
+        - /nginx-ingress-controller
+        - --configmap=$(POD_NAMESPACE)/nginx-load-balancer-microk8s-conf-e2e
+        - --tcp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-tcp-microk8s-conf-e2e
+        - --udp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-udp-microk8s-conf-e2e
+        - --election-id=ingress-controller-leader-e2e
+        - --controller-class=tfs.etsi.org/controller-class-e2e
+        - --ingress-class=tfs-ingress-class-e2e
+        - ' '
+        - --publish-status-address=127.0.0.1
diff --git a/src/tests/ecoc24/nginx-ingress-controller-ip.yaml b/src/tests/ecoc24/nginx-ingress-controller-ip.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4735ae264a27e6f691d2900701afdd2b0ef2e3d4
--- /dev/null
+++ b/src/tests/ecoc24/nginx-ingress-controller-ip.yaml
@@ -0,0 +1,138 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-load-balancer-microk8s-conf-ip
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-udp-microk8s-conf-ip
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-tcp-microk8s-conf-ip
+  namespace: ingress
+---
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: tfs-ingress-class-ip
+  annotations:
+    ingressclass.kubernetes.io/is-default-class: "false"
+spec:
+  controller: tfs.etsi.org/controller-class-ip
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: nginx-ingress-microk8s-controller-ip
+  namespace: ingress
+  labels:
+    microk8s-application: nginx-ingress-microk8s-ip
+spec:
+  selector:
+    matchLabels:
+      name: nginx-ingress-microk8s-ip
+  updateStrategy:
+    rollingUpdate:
+      maxSurge: 0
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        name: nginx-ingress-microk8s-ip
+    spec:
+      terminationGracePeriodSeconds: 60
+      restartPolicy: Always
+      serviceAccountName: nginx-ingress-microk8s-serviceaccount
+      containers:
+      - image: k8s.gcr.io/ingress-nginx/controller:v1.2.0
+        imagePullPolicy: IfNotPresent
+        name: nginx-ingress-microk8s
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command:
+                - /wait-shutdown
+        securityContext:
+          capabilities:
+            add:
+            - NET_BIND_SERVICE
+            drop:
+            - ALL
+          runAsUser: 101 # www-data
+        env:
+          - name: POD_NAME
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.name
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.namespace
+        ports:
+        - name: http
+          containerPort: 80
+          hostPort: 8002
+          protocol: TCP
+        - name: https
+          containerPort: 443
+          hostPort: 4432
+          protocol: TCP
+        - name: health
+          containerPort: 10254
+          hostPort: 12542
+          protocol: TCP
+        - name: ws
+          containerPort: 8762
+          hostPort: 8762
+          protocol: TCP
+        args:
+        - /nginx-ingress-controller
+        - --configmap=$(POD_NAMESPACE)/nginx-load-balancer-microk8s-conf-ip
+        - --tcp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-tcp-microk8s-conf-ip
+        - --udp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-udp-microk8s-conf-ip
+        - --election-id=ingress-controller-leader-ip
+        - --controller-class=tfs.etsi.org/controller-class-ip
+        - --ingress-class=tfs-ingress-class-ip
+        - ' '
+        - --publish-status-address=127.0.0.1
diff --git a/src/tests/ecoc24/nginx-ingress-controller-opt.yaml b/src/tests/ecoc24/nginx-ingress-controller-opt.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d62898b60bed4f16999ff71f3c6173fd5d334e12
--- /dev/null
+++ b/src/tests/ecoc24/nginx-ingress-controller-opt.yaml
@@ -0,0 +1,138 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-load-balancer-microk8s-conf-opt
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-udp-microk8s-conf-opt
+  namespace: ingress
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-ingress-tcp-microk8s-conf-opt
+  namespace: ingress
+---
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: tfs-ingress-class-opt
+  annotations:
+    ingressclass.kubernetes.io/is-default-class: "false"
+spec:
+  controller: tfs.etsi.org/controller-class-opt
+---
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: nginx-ingress-microk8s-controller-opt
+  namespace: ingress
+  labels:
+    microk8s-application: nginx-ingress-microk8s-opt
+spec:
+  selector:
+    matchLabels:
+      name: nginx-ingress-microk8s-opt
+  updateStrategy:
+    rollingUpdate:
+      maxSurge: 0
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        name: nginx-ingress-microk8s-opt
+    spec:
+      terminationGracePeriodSeconds: 60
+      restartPolicy: Always
+      serviceAccountName: nginx-ingress-microk8s-serviceaccount
+      containers:
+      - image: k8s.gcr.io/ingress-nginx/controller:v1.2.0
+        imagePullPolicy: IfNotPresent
+        name: nginx-ingress-microk8s
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          periodSeconds: 10
+          successThreshold: 1
+          failureThreshold: 3
+          timeoutSeconds: 5
+        lifecycle:
+          preStop:
+            exec:
+              command:
+                - /wait-shutdown
+        securityContext:
+          capabilities:
+            add:
+            - NET_BIND_SERVICE
+            drop:
+            - ALL
+          runAsUser: 101 # www-data
+        env:
+          - name: POD_NAME
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.name
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                apiVersion: v1
+                fieldPath: metadata.namespace
+        ports:
+        - name: http
+          containerPort: 80
+          hostPort: 8003
+          protocol: TCP
+        - name: https
+          containerPort: 443
+          hostPort: 4433
+          protocol: TCP
+        - name: health
+          containerPort: 10254
+          hostPort: 12543
+          protocol: TCP
+        - name: ws
+          containerPort: 8763
+          hostPort: 8763
+          protocol: TCP
+        args:
+        - /nginx-ingress-controller
+        - --configmap=$(POD_NAMESPACE)/nginx-load-balancer-microk8s-conf-opt
+        - --tcp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-tcp-microk8s-conf-opt
+        - --udp-services-configmap=$(POD_NAMESPACE)/nginx-ingress-udp-microk8s-conf-opt
+        - --election-id=ingress-controller-leader-opt
+        - --controller-class=tfs.etsi.org/controller-class-opt
+        - --ingress-class=tfs-ingress-class-opt
+        - ' '
+        - --publish-status-address=127.0.0.1
diff --git a/src/tests/ecoc24/show_deploy.sh b/src/tests/ecoc24/show_deploy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..295dd29b4d132c6f809a01e5dc2cb17a956531a2
--- /dev/null
+++ b/src/tests/ecoc24/show_deploy.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+########################################################################################################################
+# Automated steps start here
+########################################################################################################################
+
+echo "E2E Deployment Resources:"
+kubectl --namespace tfs-e2e get all
+printf "\n"
+
+echo "E2E Deployment Ingress:"
+kubectl --namespace tfs-e2e get ingress
+printf "\n"
+
+echo "IP Deployment Resources:"
+kubectl --namespace tfs-ip get all
+printf "\n"
+
+echo "IP Deployment Ingress:"
+kubectl --namespace tfs-ip get ingress
+printf "\n"
+
+echo "Optical Deployment Resources:"
+kubectl --namespace tfs-opt get all
+printf "\n"
+
+echo "Optical Deployment Ingress:"
+kubectl --namespace tfs-opt get ingress
+printf "\n"
diff --git a/src/tests/ecoc24/subscription_ws_e2e.sh b/src/tests/ecoc24/subscription_ws_e2e.sh
new file mode 100755
index 0000000000000000000000000000000000000000..33f5a5109044b1aa7384da41965d1b84a29cb945
--- /dev/null
+++ b/src/tests/ecoc24/subscription_ws_e2e.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+########################################################################################################################
+# Read deployment settings
+########################################################################################################################
+
+# If not already set, set the namespace where CockroackDB will be deployed.
+export SUBSCRIPTION_WS_NAMESPACE=${SUBSCRIPTION_WS_NAMESPACE:-"tfs-e2e"}
+
+# If not already set, set the external port interface will be exposed to.
+export SUBSCRIPTION_WS_EXT_PORT=${SUBSCRIPTION_WS_EXT_PORT:-"8761"}
+
+
+########################################################################################################################
+# Automated steps start here
+########################################################################################################################
+
+
+echo "Subscription WebSocket Port Mapping"
+echo ">>> ExposeSubscription WebSocket port (${SUBSCRIPTION_WS_EXT_PORT}->${SUBSCRIPTION_WS_EXT_PORT})"
+PATCH='{"data": {"'${SUBSCRIPTION_WS_EXT_PORT}'": "'${SUBSCRIPTION_WS_NAMESPACE}'/nbiservice:'${SUBSCRIPTION_WS_EXT_PORT}'"}}'
+kubectl patch configmap nginx-ingress-tcp-microk8s-conf-e2e --namespace ingress --patch "${PATCH}"
+
+PORT_MAP='{"containerPort": '${SUBSCRIPTION_WS_EXT_PORT}', "hostPort": '${SUBSCRIPTION_WS_EXT_PORT}'}'
+CONTAINER='{"name": "nginx-ingress-microk8s", "ports": ['${PORT_MAP}']}'
+PATCH='{"spec": {"template": {"spec": {"containers": ['${CONTAINER}']}}}}'
+kubectl patch daemonset nginx-ingress-microk8s-controller-e2e --namespace ingress --patch "${PATCH}"
+echo
diff --git a/src/tests/ecoc24/subscription_ws_ip.sh b/src/tests/ecoc24/subscription_ws_ip.sh
new file mode 100755
index 0000000000000000000000000000000000000000..795469e8fcb026cd1734a6330fc7b8c4a1e97aac
--- /dev/null
+++ b/src/tests/ecoc24/subscription_ws_ip.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+
+########################################################################################################################
+# Read deployment settings
+########################################################################################################################
+
+# If not already set, set the namespace where CockroackDB will be deployed.
+export SUBSCRIPTION_WS_NAMESPACE=${SUBSCRIPTION_WS_NAMESPACE:-"tfs-ip"}
+
+# If not already set, set the external port interface will be exposed to.
+export SUBSCRIPTION_WS_INT_PORT=${SUBSCRIPTION_WS_INT_PORT:-"8762"}
+########################################################################################################################
+# Automated steps start here
+########################################################################################################################
+
+
+
+
+echo "Subscription WebSocket Port Mapping"
+echo ">>> ExposeSubscription WebSocket port (${SUBSCRIPTION_WS_INT_PORT}->${SUBSCRIPTION_WS_INT_PORT})"
+PATCH='{"data": {"'${SUBSCRIPTION_WS_INT_PORT}'": "'${SUBSCRIPTION_WS_NAMESPACE}'/nbiservice:'${SUBSCRIPTION_WS_INT_PORT}'"}}'
+kubectl patch configmap nginx-ingress-tcp-microk8s-conf-ip --namespace ingress --patch "${PATCH}"
+
+PORT_MAP='{"containerPort": '${SUBSCRIPTION_WS_INT_PORT}', "hostPort": '${SUBSCRIPTION_WS_INT_PORT}'}'
+CONTAINER='{"name": "nginx-ingress-microk8s", "ports": ['${PORT_MAP}']}'
+PATCH='{"spec": {"template": {"spec": {"containers": ['${CONTAINER}']}}}}'
+kubectl patch daemonset nginx-ingress-microk8s-controller-ip --namespace ingress --patch "${PATCH}"
+echo
diff --git a/src/tests/ecoc24/tfs-ingress-e2e.yaml b/src/tests/ecoc24/tfs-ingress-e2e.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1b82ae44ca0945163701bc18a250a1742e921519
--- /dev/null
+++ b/src/tests/ecoc24/tfs-ingress-e2e.yaml
@@ -0,0 +1,67 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: tfs-ingress-e2e
+  annotations:
+    nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+  ingressClassName: tfs-ingress-class-e2e
+  rules:
+    - http:
+        paths:
+          - path: /webui(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 8004
+          - path: /grafana(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 3000
+          - path: /()(restconf/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(tfs-api/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(bmw/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(qkd_app/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
diff --git a/src/tests/ecoc24/tfs-ingress-ip.yaml b/src/tests/ecoc24/tfs-ingress-ip.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6ee6ada32f3f278c5f36dfbb08d936a432bfef23
--- /dev/null
+++ b/src/tests/ecoc24/tfs-ingress-ip.yaml
@@ -0,0 +1,67 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: tfs-ingress-ip
+  annotations:
+    nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+  ingressClassName: tfs-ingress-class-ip
+  rules:
+    - http:
+        paths:
+          - path: /webui(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 8004
+          - path: /grafana(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 3000
+          - path: /()(restconf/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(tfs-api/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(bmw/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(qkd_app/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
diff --git a/src/tests/ecoc24/tfs-ingress-opt.yaml b/src/tests/ecoc24/tfs-ingress-opt.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..50ddd4f046acf74cfe57e73f55e29e1b77fd32af
--- /dev/null
+++ b/src/tests/ecoc24/tfs-ingress-opt.yaml
@@ -0,0 +1,67 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: tfs-ingress-opt
+  annotations:
+    nginx.ingress.kubernetes.io/rewrite-target: /$2
+spec:
+  ingressClassName: tfs-ingress-class-opt
+  rules:
+    - http:
+        paths:
+          - path: /webui(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 8004
+          - path: /grafana(/|$)(.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: webuiservice
+                port:
+                  number: 3000
+          - path: /()(restconf/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(tfs-api/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(bmw/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
+          - path: /()(qkd_app/.*)
+            pathType: Prefix
+            backend:
+              service:
+                name: nbiservice
+                port:
+                  number: 8080
diff --git a/src/vnt_manager/.gitlab-ci.yml b/src/vnt_manager/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b338f97ac8a517abbfbdc5112bd54d5d6545afd1
--- /dev/null
+++ b/src/vnt_manager/.gitlab-ci.yml
@@ -0,0 +1,38 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+# build, tag and push the Docker image to the gitlab registry
+build vntmanager:
+  variables:
+    IMAGE_NAME: 'vntmanager' # name of the microservice
+    IMAGE_TAG: 'latest' # tag of the container image (production, development, etc)
+  stage: build
+  before_script:
+    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+  script:
+    - docker buildx build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile .
+    - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG"
+    - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG"
+  after_script:
+    - docker images --filter="dangling=true" --quiet | xargs -r docker rmi
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)'
+    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' 
+    - changes:
+      - src/$IMAGE_NAME/**/*.{py,in,yml}
+      - src/$IMAGE_NAME/Dockerfile
+      - src/$IMAGE_NAME/tests/*.py
+      - src/$IMAGE_NAME/tests/Dockerfile
+      - manifests/${IMAGE_NAME}service.yaml
+      - .gitlab-ci.yml
diff --git a/src/vnt_manager/Config.py b/src/vnt_manager/Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02
--- /dev/null
+++ b/src/vnt_manager/Config.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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/vnt_manager/Dockerfile b/src/vnt_manager/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..2680336e962240c3938b06ffb47079bd6dab1368
--- /dev/null
+++ b/src/vnt_manager/Dockerfile
@@ -0,0 +1,87 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+FROM python:3.9-slim
+
+# Install dependencies
+RUN apt-get --yes --quiet --quiet update && \
+    apt-get --yes --quiet --quiet install wget g++ && \
+    rm -rf /var/lib/apt/lists/*
+
+# Set Python to show logs as they occur
+ENV PYTHONUNBUFFERED=0
+ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
+
+# Download the gRPC health probe
+RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \
+    wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
+    chmod +x /bin/grpc_health_probe
+
+# Creating a user for security reasons
+RUN groupadd -r teraflow && useradd -u 1001 --no-log-init -r -m -g teraflow teraflow
+USER teraflow
+
+# set working directory
+RUN mkdir -p /home/teraflow/controller/common/
+WORKDIR /home/teraflow/controller
+
+# Get Python packages per module
+ENV VIRTUAL_ENV=/home/teraflow/venv
+RUN python3 -m venv ${VIRTUAL_ENV}
+ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
+
+# Get generic Python packages
+RUN python3 -m pip install --upgrade pip
+RUN python3 -m pip install --upgrade setuptools wheel
+RUN python3 -m pip install --upgrade pip-tools
+
+# Get common Python packages
+# Note: this step enables sharing the previous Docker build steps among all the Python components
+COPY --chown=teraflow:teraflow common_requirements.in common_requirements.in
+RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in
+RUN python3 -m pip install -r common_requirements.txt
+
+# Add common files into working directory
+WORKDIR /home/teraflow/controller/common
+COPY --chown=teraflow:teraflow src/common/. ./
+RUN rm -rf proto
+
+# Create proto sub-folder, copy .proto files, and generate Python code
+RUN mkdir -p /home/teraflow/controller/common/proto
+WORKDIR /home/teraflow/controller/common/proto
+RUN touch __init__.py
+COPY --chown=teraflow:teraflow proto/*.proto ./
+RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto
+RUN rm *.proto
+RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \;
+
+# Create module sub-folders
+RUN mkdir -p /home/teraflow/controller/vnt_manager
+WORKDIR /home/teraflow/controller
+
+# Get Python packages per module
+COPY --chown=teraflow:teraflow ./src/vnt_manager/requirements.in vnt_manager/requirements.in
+# consider common and specific requirements to avoid inconsistencies with dependencies
+RUN pip-compile --quiet --output-file=vnt_manager/requirements.txt vnt_manager/requirements.in common_requirements.in
+RUN python3 -m pip install -r vnt_manager/requirements.txt
+
+# Add component files into working directory
+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/vnt_manager/. vnt_manager/
+
+# Start the service
+ENTRYPOINT ["python", "-m", "vnt_manager.service"]
diff --git a/src/vnt_manager/__init__.py b/src/vnt_manager/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02
--- /dev/null
+++ b/src/vnt_manager/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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/vnt_manager/client/VNTManagerClient.py b/src/vnt_manager/client/VNTManagerClient.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ecb3dc0d8f7c937c66b5af6e2f3ae715575b425
--- /dev/null
+++ b/src/vnt_manager/client/VNTManagerClient.py
@@ -0,0 +1,104 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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
+
+import grpc
+
+from common.Constants import ServiceNameEnum
+from common.proto.context_pb2 import Empty
+from common.proto.vnt_manager_pb2 import VNTSubscriptionRequest, VNTSubscriptionReply
+from common.proto.vnt_manager_pb2_grpc import VNTManagerServiceStub
+from common.Settings import get_service_host, get_service_port_grpc
+from common.tools.client.RetryDecorator import delay_exponential, retry
+from common.tools.grpc.Tools import grpc_message_to_json
+from common.proto.context_pb2 import (
+    Link, LinkId, LinkIdList, LinkList,
+)
+from common.tools.grpc.Tools import grpc_message_to_json_string
+
+LOGGER = logging.getLogger(__name__)
+MAX_RETRIES = 15
+DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0)
+RETRY_DECORATOR = retry(
+    max_retries=MAX_RETRIES,
+    delay_function=DELAY_FUNCTION,
+    prepare_method_name="connect",
+)
+
+
+class VNTManagerClient:
+    def __init__(self, host=None, port=None):
+        if not host:
+            host = get_service_host(ServiceNameEnum.VNTMANAGER)
+        if not port:
+            port = get_service_port_grpc(ServiceNameEnum.VNTMANAGER)
+        self.endpoint = "{:s}:{:s}".format(str(host), str(port))
+        LOGGER.debug("Creating channel to {:s}...".format(str(self.endpoint)))
+        self.channel = None
+        self.stub = None
+        self.connect()
+        LOGGER.debug("Channel created")
+
+    def connect(self):
+        self.channel = grpc.insecure_channel(self.endpoint)
+        self.stub = VNTManagerServiceStub(self.channel)
+
+    def close(self):
+        if self.channel is not None:
+            self.channel.close()
+        self.channel = None
+        self.stub = None
+
+    @RETRY_DECORATOR
+    def VNTSubscript(self, request: VNTSubscriptionRequest) -> VNTSubscriptionReply:
+        LOGGER.debug("Subscript request: {:s}".format(str(grpc_message_to_json(request))))
+        response = self.stub.VNTSubscript(request)
+        LOGGER.debug("Subscript result: {:s}".format(str(grpc_message_to_json(response))))
+        return response
+
+    @RETRY_DECORATOR
+    def ListVirtualLinkIds(self, request: Empty) -> LinkIdList:
+        LOGGER.debug('ListVirtualLinkIds request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.ListVirtualLinkIds(request)
+        LOGGER.debug('ListVirtualLinkIds result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
+
+    @RETRY_DECORATOR
+    def ListVirtualLinks(self, request: Empty) -> LinkList:
+        LOGGER.debug('ListVirtualLinks request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.ListVirtualLinks(request)
+        LOGGER.debug('ListVirtualLinks result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
+
+    @RETRY_DECORATOR
+    def GetVirtualLink(self, request: LinkId) -> Link:
+        LOGGER.debug('GetVirtualLink request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.GetVirtualLink(request)
+        LOGGER.debug('GetVirtualLink result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
+
+    @RETRY_DECORATOR
+    def SetVirtualLink(self, request: Link) -> LinkId:
+        LOGGER.debug('SetVirtualLink request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.SetVirtualLink(request)
+        LOGGER.debug('SetVirtualLink result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
+
+    @RETRY_DECORATOR
+    def RemoveVirtualLink(self, request: LinkId) -> Empty:
+        LOGGER.debug('RemoveVirtualLink request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.RemoveVirtualLink(request)
+        LOGGER.debug('RemoveVirtualLink result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
diff --git a/src/vnt_manager/client/__init__.py b/src/vnt_manager/client/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02
--- /dev/null
+++ b/src/vnt_manager/client/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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/vnt_manager/requirements.in b/src/vnt_manager/requirements.in
new file mode 100644
index 0000000000000000000000000000000000000000..6f9f590845cf3c925b2da23f127bc4aa942253b9
--- /dev/null
+++ b/src/vnt_manager/requirements.in
@@ -0,0 +1,15 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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.
+
+websockets==12.0
diff --git a/src/vnt_manager/service/VNTManagerService.py b/src/vnt_manager/service/VNTManagerService.py
new file mode 100644
index 0000000000000000000000000000000000000000..80d635ffe475c7840cf9bd0c0650471bae17cb3e
--- /dev/null
+++ b/src/vnt_manager/service/VNTManagerService.py
@@ -0,0 +1,35 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 common.Constants import ServiceNameEnum
+from common.proto.vnt_manager_pb2_grpc import add_VNTManagerServiceServicer_to_server
+from common.Settings import get_service_port_grpc
+from common.tools.service.GenericGrpcService import GenericGrpcService
+from .VNTManagerServiceServicerImpl import VNTManagerServiceServicerImpl
+
+LOGGER = logging.getLogger(__name__)
+
+
+class VNTManagerService(GenericGrpcService):
+    def __init__(self, cls_name: str = __name__):
+        port = get_service_port_grpc(ServiceNameEnum.VNTMANAGER)
+        super().__init__(port, cls_name=cls_name)
+        self.vntmanager_servicer = VNTManagerServiceServicerImpl()
+
+    def install_servicers(self):
+        add_VNTManagerServiceServicer_to_server(
+            self.vntmanager_servicer, self.server
+        )
diff --git a/src/vnt_manager/service/VNTManagerServiceServicerImpl.py b/src/vnt_manager/service/VNTManagerServiceServicerImpl.py
new file mode 100644
index 0000000000000000000000000000000000000000..d684e044efd972bd7705f6c1a448b0a5be23431b
--- /dev/null
+++ b/src/vnt_manager/service/VNTManagerServiceServicerImpl.py
@@ -0,0 +1,183 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 grpc
+import json
+import logging
+import threading
+import time
+from websockets.sync.client import connect
+from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
+from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method
+from common.proto.context_pb2 import ContextId, Empty, Link, LinkId, LinkList, TopologyId
+from common.proto.vnt_manager_pb2 import VNTSubscriptionRequest, VNTSubscriptionReply
+from common.proto.vnt_manager_pb2_grpc import VNTManagerServiceServicer
+from common.tools.grpc.Tools import grpc_message_to_json, grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from common.tools.object_factory.Topology import json_topology_id
+from context.client.ContextClient import ContextClient
+from context.client.EventsCollector import EventsCollector
+from .vntm_config_device import configure, deconfigure
+
+LOGGER = logging.getLogger(__name__)
+
+METRICS_POOL = MetricsPool("VNTManager", "RPC")
+
+context_client: ContextClient = ContextClient()
+
+JSON_ADMIN_CONTEXT_ID = json_context_id(DEFAULT_CONTEXT_NAME)
+ADMIN_CONTEXT_ID = ContextId(**JSON_ADMIN_CONTEXT_ID)
+ADMIN_TOPOLOGY_ID = TopologyId(**json_topology_id(DEFAULT_TOPOLOGY_NAME, context_id=JSON_ADMIN_CONTEXT_ID))
+
+GET_EVENT_TIMEOUT = 0.5
+
+
+class VNTMEventDispatcher(threading.Thread):
+    def __init__(self, host, port) -> None:
+        LOGGER.debug('Creating VTNM connector...')
+        self.host = host
+        self.port = port
+        super().__init__(name='VNTMEventDispatcher', daemon=True)
+        self._terminate = threading.Event()
+        LOGGER.debug('VNTM connector created')
+
+    def start(self) -> None:
+        self._terminate.clear()
+        return super().start()
+
+    def stop(self):
+        self._terminate.set()
+
+    
+    def send_msg(self, msg):
+        try:
+            self.websocket.send(msg)
+        except Exception as e:
+            LOGGER.info(e)
+
+    def recv_msg(self):
+        message = self.websocket.recv()
+        return message
+
+    def run(self) -> None:
+
+        time.sleep(5)
+        events_collector = EventsCollector(
+            context_client, log_events_received=True,
+            activate_context_collector     = False,
+            activate_topology_collector    = True,
+            activate_device_collector      = False,
+            activate_link_collector        = False,
+            activate_service_collector     = False,
+            activate_slice_collector       = False,
+            activate_connection_collector  = False,)
+        events_collector.start()
+
+
+        url = "ws://" + str(self.host) + ":" + str(self.port)
+        LOGGER.debug('Connecting to {}'.format(url))
+
+        try:
+            LOGGER.info("Connecting to events server...: {}".format(url))
+            self.websocket = connect(url)
+        except Exception as ex:
+            LOGGER.error('Error connecting to {}'.format(url))
+        else:
+            LOGGER.info('Connected to {}'.format(url))
+            context_id = json_context_id(DEFAULT_CONTEXT_NAME)
+            topology_id = json_topology_id(DEFAULT_TOPOLOGY_NAME, context_id)
+            
+            try:
+                topology_details = context_client.GetTopologyDetails(TopologyId(**topology_id))
+            except Exception as ex:
+                LOGGER.warning('No topology found')
+            else:
+                self.send_msg(grpc_message_to_json_string(topology_details))
+
+            while not self._terminate.is_set():
+                event = events_collector.get_event(block=True, timeout=GET_EVENT_TIMEOUT)
+                LOGGER.info('Event type: {}'.format(event))
+                if event is None: continue
+                LOGGER.debug('Received event: {}'.format(event))
+                topology_details = context_client.GetTopologyDetails(TopologyId(**topology_id))
+
+                to_send = grpc_message_to_json_string(topology_details)
+
+                self.send_msg(to_send)
+        
+            LOGGER.info('Exiting')
+            events_collector.stop()
+
+
+class VNTManagerServiceServicerImpl(VNTManagerServiceServicer):
+    def __init__(self):
+        LOGGER.debug("Creating Servicer...")
+        LOGGER.debug("Servicer Created")
+        self.links = []
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def VNTSubscript(self, request: VNTSubscriptionRequest, context: grpc.ServicerContext) -> VNTSubscriptionReply:
+        LOGGER.info("Subscript request: {:s}".format(str(grpc_message_to_json(request))))
+        reply = VNTSubscriptionReply()
+        reply.subscription = "OK"
+
+        self.event_dispatcher = VNTMEventDispatcher(request.host, int(request.port))
+        self.host = request.host
+        self.port = request.port
+        self.event_dispatcher.start()
+        return reply
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def ListVirtualLinks(self, request : Empty, context : grpc.ServicerContext) -> LinkList:
+        return [link for link in context_client.ListLinks(Empty()).links if link.virtual]
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def GetVirtualLink(self, request : LinkId, context : grpc.ServicerContext) -> Link:
+        link = context_client.GetLink(request)
+        return link if link.virtual else Empty()
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def SetVirtualLink(self, request : Link, context : grpc.ServicerContext) -> LinkId:
+        try:
+            LOGGER.info('SETTING virtual link')
+            self.event_dispatcher.send_msg(grpc_message_to_json_string(request))
+            # configure('CSGW1', 'xe5', 'CSGW2', 'xe5', 'ecoc2024-1')
+            response = self.event_dispatcher.recv_msg()
+            message_json = json.loads(response)
+            link = Link(**message_json)
+            context_client.SetLink(link)
+        except Exception as e:
+            LOGGER.error('Exception setting virtual link={}\n\t{}'.format(request.link_id.link_uuid.uuid, e))
+        return request.link_id
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def RemoveVirtualLink(self, request : LinkId, context : grpc.ServicerContext) -> Empty:
+        try:
+            self.event_dispatcher.send_msg(grpc_message_to_json_string(request))
+            # deconfigure('CSGW1', 'xe5', 'CSGW2', 'xe5', 'ecoc2024-1')
+            response = self.event_dispatcher.recv_msg()
+            message_json = json.loads(response)
+            link_id = LinkId(**message_json)
+            context_client.RemoveLink(link_id)
+
+            LOGGER.info('Removed')
+        except Exception as e:
+            msg_error = 'Exception removing virtual link={}\n\t{}'.format(request.link_uuid.uuid, e)
+            LOGGER.error(msg_error)
+            return msg_error
+        else:
+            context_client.RemoveLink(request)
+            LOGGER.info('Removed')
+
+        return Empty()
diff --git a/src/vnt_manager/service/__init__.py b/src/vnt_manager/service/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02
--- /dev/null
+++ b/src/vnt_manager/service/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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/vnt_manager/service/__main__.py b/src/vnt_manager/service/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a67eb4cfdf657c00373ce93d617425eda2f00981
--- /dev/null
+++ b/src/vnt_manager/service/__main__.py
@@ -0,0 +1,66 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, signal, sys, threading
+from prometheus_client import start_http_server
+from common.Constants import ServiceNameEnum
+from common.Settings import (
+    ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC,
+    get_env_var_name, get_log_level, get_metrics_port,
+    wait_for_environment_variables
+)
+from .VNTManagerService import VNTManagerService
+
+LOG_LEVEL = get_log_level()
+logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s")
+LOGGER = logging.getLogger(__name__)
+
+terminate = threading.Event()
+
+def signal_handler(signal, frame): # pylint: disable=redefined-outer-name,unused-argument
+    LOGGER.warning("Terminate signal received")
+    terminate.set()
+
+def main():
+    LOGGER.info("Starting...")
+
+    wait_for_environment_variables([
+        get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.DEVICE,  ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.DEVICE,  ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+    ])
+
+    signal.signal(signal.SIGINT,  signal_handler)
+    signal.signal(signal.SIGTERM, signal_handler)
+
+    # Start metrics server
+    metrics_port = get_metrics_port()
+    start_http_server(metrics_port)
+
+    # Starting VNTManager service
+    grpc_service = VNTManagerService()
+    grpc_service.start()
+
+    # Wait for Ctrl+C or termination signal
+    while not terminate.wait(timeout=1): pass
+
+    LOGGER.info("Terminating...")
+    grpc_service.stop()
+
+    LOGGER.info("Bye")
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/src/vnt_manager/service/vntm_config_device.py b/src/vnt_manager/service/vntm_config_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..4735ed31f185ba221033a7611b2b1af3f90c1688
--- /dev/null
+++ b/src/vnt_manager/service/vntm_config_device.py
@@ -0,0 +1,184 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from typing import Dict
+from common.proto.context_pb2 import ConfigRule
+from common.tools.context_queries.Device import get_device
+from common.tools.object_factory.ConfigRule import json_config_rule_set, json_config_rule_delete
+from context.client.ContextClient import ContextClient
+from device.client.DeviceClient import DeviceClient
+
+##### Config Rule Composers ####################################################
+
+def compose_config_rule(resource_key, resource_value, delete) -> Dict:
+    json_config_rule = json_config_rule_delete if delete else json_config_rule_set
+    return ConfigRule(**json_config_rule(resource_key, resource_value))
+
+def network_instance(ni_name, ni_type, ni_router_id=None, ni_route_distinguisher=None, delete=False) -> Dict:
+    path = '/network_instance[{:s}]'.format(ni_name)
+    data = {'name': ni_name, 'type': ni_type}
+    if ni_router_id is not None: data['router_id'] = ni_router_id
+    if ni_route_distinguisher is not None: data['route_distinguisher'] = ni_route_distinguisher
+    return compose_config_rule(path, data, delete)
+
+def network_instance_add_protocol_bgp(ni_name, ni_type, ni_router_id, ni_bgp_as, neighbors=[], delete=False)-> Dict:
+    path = '/network_instance[{:s}]/protocols[BGP]'.format(ni_name)
+    data = {
+        'name': ni_name, 'type': ni_type, 'router_id': ni_router_id, 'identifier': 'BGP',
+        'protocol_name': ni_bgp_as, 'as': ni_bgp_as
+    }
+    if len(neighbors) > 0:
+        data['neighbors'] = [
+            {'ip_address': neighbor_ip_address, 'remote_as': neighbor_remote_as}
+            for neighbor_ip_address, neighbor_remote_as in neighbors
+        ]
+    return compose_config_rule(path, data, delete)
+
+def network_instance_add_protocol_direct(ni_name, ni_type, delete=False) -> Dict:
+    path = '/network_instance[{:s}]/protocols[DIRECTLY_CONNECTED]'.format(ni_name)
+    data = {
+        'name': ni_name, 'type': ni_type, 'identifier': 'DIRECTLY_CONNECTED',
+        'protocol_name': 'DIRECTLY_CONNECTED'
+    }
+    return compose_config_rule(path, data, delete)
+
+def network_instance_add_protocol_static(ni_name, ni_type, delete=False) -> Dict:
+    path = '/network_instance[{:s}]/protocols[STATIC]'.format(ni_name)
+    data = {
+        'name': ni_name, 'type': ni_type, 'identifier': 'STATIC',
+        'protocol_name': 'STATIC'
+    }
+    return compose_config_rule(path, data, delete)
+
+def network_instance_add_table_connection(
+    ni_name, src_protocol, dst_protocol, address_family, default_import_policy, bgp_as=None, delete=False
+) -> Dict:
+    path = '/network_instance[{:s}]/table_connections[{:s}][{:s}][{:s}]'.format(
+        ni_name, src_protocol, dst_protocol, address_family
+    )
+    data = {
+        'name': ni_name, 'src_protocol': src_protocol, 'dst_protocol': dst_protocol,
+        'address_family': address_family, 'default_import_policy': default_import_policy,
+    }
+    if bgp_as is not None: data['as'] = bgp_as
+    return compose_config_rule(path, data, delete)
+
+def interface(
+    name, index, description=None, if_type=None, vlan_id=None, mtu=None, ipv4_address_prefix=None,
+    enabled=None, delete=False
+) -> Dict:
+    path = '/interface[{:s}]/subinterface[{:d}]'.format(name, index)
+    data = {'name': name, 'index': index}
+    if description is not None: data['description'] = description
+    if if_type     is not None: data['type'       ] = if_type
+    if vlan_id     is not None: data['vlan_id'    ] = vlan_id
+    if mtu         is not None: data['mtu'        ] = mtu
+    if enabled     is not None: data['enabled'    ] = enabled
+    if ipv4_address_prefix is not None:
+        ipv4_address, ipv4_prefix = ipv4_address_prefix
+        data['address_ip'    ] = ipv4_address
+        data['address_prefix'] = ipv4_prefix
+    return compose_config_rule(path, data, delete)
+
+def network_instance_interface(ni_name, ni_type, if_name, if_index, delete=False) -> Dict:
+    path = '/network_instance[{:s}]/interface[{:s}.{:d}]'.format(ni_name, if_name, if_index)
+    data = {'name': ni_name, 'type': ni_type, 'id': if_name, 'interface': if_name, 'subinterface': if_index}
+    return compose_config_rule(path, data, delete)
+
+# configure('CSGW1', 'xe5', 'CSGW2', 'xe5', 'ecoc2024-1')
+# deconfigure('CSGW1', 'xe5', 'CSGW2', 'xe5', 'ecoc2024-1')
+
+def configure(router_a, port_a, router_b, port_b, ni_name):
+    context_client = ContextClient()
+    device_client = DeviceClient()
+
+    client_if_name       = 'ce1'
+    client_if_addr       = {'CSGW1': ('192.168.10.1', 24), 'CSGW2': ('192.168.20.1', 24)}
+    bgp_router_addresses = {'CSGW1': '192.168.150.1', 'CSGW2': '192.168.150.2'}
+
+    locations = [
+        {'router': router_a, 'port': port_a, 'neighbor': router_b},
+        {'router': router_b, 'port': port_b, 'neighbor': router_a},
+    ]
+    for location in locations:
+        router   = location['router']
+        port     = location['port']
+        neighbor = location['neighbor']
+
+        client_ipv4_address_prefix = client_if_addr[router]
+        bgp_router_address         = bgp_router_addresses[router]
+        bgp_neighbor_address       = bgp_router_addresses[neighbor]
+
+        config_rules = [
+            network_instance(ni_name, 'L3VRF', bgp_router_address, '65001:1'),
+            network_instance_add_protocol_direct(ni_name, 'L3VRF'),
+            network_instance_add_protocol_static(ni_name, 'L3VRF'),
+            network_instance_add_protocol_bgp(ni_name, 'L3VRF', bgp_router_address, '65001', neighbors=[
+                (bgp_neighbor_address, '65001')
+            ]),
+            network_instance_add_table_connection(
+                ni_name, 'DIRECTLY_CONNECTED', 'BGP', 'IPV4', 'ACCEPT_ROUTE', bgp_as='65001'
+            ),
+            network_instance_add_table_connection(
+                ni_name, 'STATIC', 'BGP', 'IPV4', 'ACCEPT_ROUTE', bgp_as='65001'
+            ),
+        
+            interface(client_if_name, 0, if_type='ethernetCsmacd', mtu=1500),
+            network_instance_interface(ni_name, 'L3VRF', client_if_name, 0),
+            interface(client_if_name, 0, if_type='ethernetCsmacd', mtu=1500,
+                    ipv4_address_prefix=client_ipv4_address_prefix, enabled=True),
+
+            interface(port, 0, if_type='ethernetCsmacd', mtu=1500),
+            network_instance_interface(ni_name, 'L3VRF', port, 0),
+            interface(port, 0, if_type='ethernetCsmacd', mtu=1500,
+                      ipv4_address_prefix=(bgp_router_address, 24), enabled=True),
+        ]
+
+        device = get_device(
+            context_client, router, rw_copy=True, include_endpoints=False,
+            include_config_rules=False, include_components=False
+        )
+        device.device_config.config_rules.extend(config_rules)
+        device_client.ConfigureDevice(device)
+
+
+def deconfigure(router_a, port_a, router_b, port_b, ni_name):
+    context_client = ContextClient()
+    device_client = DeviceClient()
+
+    client_if_name = 'ce1'
+
+    locations = [
+        {'router': router_a, 'port': port_a, 'neighbor': router_b},
+        {'router': router_b, 'port': port_b, 'neighbor': router_a},
+    ]
+    for location in locations:
+        router   = location['router']
+        port     = location['port']
+        #neighbor = location['neighbor']
+
+        config_rules = [
+            network_instance_interface(ni_name, 'L3VRF', client_if_name, 0, delete=True),
+            network_instance_interface(ni_name, 'L3VRF', port, 0, delete=True),
+            #interface(client_if_name, 0, delete=True),
+            #interface(port, 0, delete=True),
+            network_instance(ni_name, 'L3VRF', delete=True),
+        ]
+
+        device = get_device(
+            context_client, router, rw_copy=True, include_endpoints=False,
+            include_config_rules=False, include_components=False
+        )
+        device.device_config.config_rules.extend(config_rules)
+        device_client.ConfigureDevice(device)