Update from master part 2

Squashed commit of the following:

commit 364627c364a86a85696781766326dd690a362bc4
Author: vegall <lvega@whitestack.com>
Date:   Fri Mar 17 15:09:50 2023 +0000

    Feature 10972: Support of volume multi-attach

    Change-Id: I6e88ee52e5e882dbb4ec7d66cf648fbe07d40509
    Signed-off-by: vegall <lvega@whitestack.com>

commit 0e51779fd37dc5c12f3bd19d78f7341ed0a67b7a
Author: gifrerenom <lluis.gifre@cttc.es>
Date:   Tue Apr 18 16:38:42 2023 +0000

    Feature 10937: Transport API (TAPI) WIM connector for RO

    Change-Id: If0dac9f8ba2d00945eb86a89fb0b2f174c672794
    Signed-off-by: gifrerenom <lluis.gifre@cttc.es>

commit 370e36bafdcb90f212e289b87290f39be141b3d4
Author: elumalai <deepika.e@tataelxsi.co.in>
Date:   Tue Apr 25 16:22:56 2023 +0530

    Feature 10979: Static IPv6 Dual Stack Assignment

    Added support for static dual stack IP assignment

    Change-Id: Ief10ae955fb870a3417f68e1c5f7bda570cb6470
    Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>

commit b1bc66933aa392b9d7518f7cebc711700335389c
Author: Gabriel Cuba <gcuba@whitestack.com>
Date:   Fri Aug 19 18:23:00 2022 -0500

    Fix Bug 2098: Get VDUs from VNFR when Heal op has no additionalPrameters

    When Heal is requested without vdu or count-index parameters, RO will recreate all VDUs from VNFR

    Change-Id: Idf2cf158bcb33e7b0c307298c14504cc7aa77e2a
    Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>
    (cherry picked from commit 2fbb3a264e4117f4a6569fede6558836d67ac4a4)

commit aba1518f487b4b65861eb30f553c4edb72ad972e
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date:   Mon May 15 11:55:13 2023 +0300

    Fix VimAdminThread run method

    The run_coroutine_threadsafe() function is used to schedule a coroutine object from a different thread and returns a concurrent.futures.Future.
    run_coroutine_threadsafe is unnecessary to run the main task and replaced with asyncio.run().

    Change-Id: I8ea31828a9798140d596165443bdf26659b4eef8
    Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>

commit f17e5bb6b6da4432628dd65ce9ad633e6441f67c
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date:   Wed May 10 22:52:57 2023 +0300

    Minor updates in Dockerfile

    Change-Id: I79b43654d181f6976a4e544d58fb92aa1b67e760
    Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>

commit a264b7a460b28d7454fc95fe659da46f55b0c155
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date:   Tue May 9 14:57:22 2023 +0300

    Ubuntu 22.04 and Python 3.10 preparation

    Change-Id: I87164827a8849c16b5e3a804d9673a578e5a5593
    Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>

commit 1c89c08a0dd1c79b5adff3ac1cc123239762e06a
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date:   Tue Apr 18 15:06:30 2023 +0200

    Clean stage-archive.sh and use allowlist_extenals in tox.ini

    Change-Id: I18f0bc3e263063b5b1d2cf211f028f6bb0e4bceb
    Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

commit 51e72a0f7479b3064b4b11891eb524d42f4738b0
Author: elumalai <deepika.e@tataelxsi.co.in>
Date:   Fri Apr 28 19:41:49 2023 +0530

    Coverity CWE 330: Use of Insufficiently Random Values

    Added support to fix CWE 330: Use of Insufficiently Random Values
    Coverity issue

    Change-Id: Ib12ebeeb9b0cc10af9980fe8661eb6230c2f6d6d
    Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>

commit e17cd946aed699b5feca83d37591d04f129a8f52
Author: elumalai <deepika.e@tataelxsi.co.in>
Date:   Fri Apr 28 18:04:24 2023 +0530

    Coverity CWE 688: Function Call With Incorrect Variable or Reference as Argument

    Added fix for CWE 688 Typo in Identifier

    Change-Id: I53b5142451b809be638d73626265531057722169
    Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>

commit 730cfaff466fb3c9b1446ecef5213916195ff861
Author: Gabriel Cuba <gcuba@whitestack.com>
Date:   Mon Mar 13 22:26:38 2023 -0500

    Feature 10975: get flavor-id from additionalParams if specified

    Change-Id: I1c9b1ec43c80f3793b475187681f4c2005d77375
    Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>

commit 2d3f63b055e6a38e95bcff56a8ddef32767b11ef
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date:   Tue Apr 11 10:08:26 2023 +0200

    Update stage-build to run tox sequentially

    Change-Id: I967f19a8c35700290e93c9d8bd863b63b7c2d239
    Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
    (cherry picked from commit ea063c7a6ae6a5d7e11e8c22f9707d5c8f674ac7)

commit b3dbfcad6f4b2bebc9ebc20fd7129a18879cb20c
Author: Gabriel Cuba <gcuba@whitestack.com>
Date:   Tue Mar 14 10:58:39 2023 -0500

    Feature 10978: Add support of ipv6_address_mode and ipv6_ra_mode to openstack connector

    Change-Id: I8ca741a215bd2c52999dee1ea301d4e02aafcb24
    Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>

commit 01619d5b596e01ac8cd6d27bf01a1174e6b3f97b
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date:   Wed Mar 22 22:57:26 2023 +0300

    Keep vim_details while reporting VM deletion

    Change-Id: I27577b2fc93a585affc947abcec8352562f23f49
    Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>

commit 98740c03567ff8c5a22f06fd3f049248a9e5f98d
Author: Pedro Escaleira <escaleira@av.it.pt>
Date:   Wed Feb 22 10:48:52 2023 +0000

    Bug 2217 fixed: modified the cloud-init merge configs and defined the default SSH keys within the system_info instead of users

    Change-Id: I12e26a88fb9b50c4a78b9fa8ee2cb5d4b4bf6d00
    Signed-off-by: Pedro Escaleira <escaleira@av.it.pt>

commit d586d89bde00acaf22debd7f657d605c9d095571
Author: Gulsum Atici <gulsum.atici@canonical.com>
Date:   Mon Feb 13 18:40:03 2023 +0300

    Feature 10960 Performance optimizations for the polling of VM status in RO

    Change-Id: If785bbeaab66e0839541bf94184ce37114e67bd4
    Signed-off-by: Gulsum Atici <gulsum.atici@canonical.com>

commit 4c1dd54ae02e82f11a60058a1b7c7b0137ac572e
Author: Gabriel Cuba <gcuba@whitestack.com>
Date:   Tue Feb 14 12:43:32 2023 -0500

    Refactor ns.py so that RO uses the IP profile as it comes from LCM

    Change-Id: I36983c86d7c76ad8a9b93eb6eae254f844717e0e
    Signed-off-by: Gabriel Cuba <gcuba@whitestack.com>

commit 3822010a26b2e21290b6acdf288db277c7f36605
Author: garciadeblas <gerardo.garciadeblas@telefonica.com>
Date:   Mon Feb 13 17:48:32 2023 +0100

    Fix bug 2216 to remove hardcoded numa affinity in VIO

    Change-Id: I0912c2841e7c5c1febe056ba092afedaea77f6a1
    Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

commit 778f3cc8c052bd17d0da32f07b880616d25f935a
Author: Lovejeet Singh <lovejeet.singh@hsc.com>
Date:   Mon Feb 13 16:15:40 2023 +0530

    Bug 2202: Adding support for cinder V3 API with V2 API for persistent volumes.

    Change-Id: I7034564b91b94e6be242cb2ce0f4a5b147b87d64
    Signed-off-by: Lovejeet Singh <lovejeet.singh@hsc.com>

Change-Id: I7ac1bd1d9896788812f456c678b1f5222a1f1ad6
Signed-off-by: Dario Faccin <dario.faccin@canonical.com>
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py
new file mode 100644
index 0000000..ab3006a
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+#######################################################################################
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py
new file mode 100644
index 0000000..a9808ba
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains the WIM settings for the unit test used to validate the
+Transport API (TAPI) WIM connector."""
+
+
+from osm_rosdn_tapi.tests.tools import wim_port_mapping
+
+
+WIM_HOST_PORT = ("127.0.0.127", 49000)
+
+# WIM_URL should be populated with the WIM url provided for the WIM connector during its instantiation
+WIM_URL = "http://{:s}:{:d}".format(*WIM_HOST_PORT)
+
+# WIM_ACCOUNT should be populated with the WIM credentials provided for the WIM connector during its instantiation
+WIM_ACCOUNT = {"user": "admin", "password": "admin"}
+
+# WIM_PORT_MAPPING should be populated with the port mapping provided for the WIM connector during its instantiation
+# In this example, SIPs are taken from mock_tapi_handler.py file.
+WIM_PORT_MAPPING = [
+    wim_port_mapping(
+        "dc1",
+        "dc1r1",
+        "eth0",
+        "R1-eth0",
+        service_mapping_info={},
+    ),
+    wim_port_mapping(
+        "dc2",
+        "dc2r2",
+        "eth0",
+        "R2-eth0",
+        service_mapping_info={},
+    ),
+    wim_port_mapping(
+        "dc3",
+        "dc3r3",
+        "eth0",
+        "R3-opt1",
+        service_mapping_info={
+            "sip_input": "R3-opt1-rx",
+            "sip_output": "R3-opt1-tx",
+        },
+    ),
+    wim_port_mapping(
+        "dc4",
+        "dc4r4",
+        "eth0",
+        "R4-opt1",
+        service_mapping_info={
+            "sip_input": "R4-opt1-rx",
+            "sip_output": "R4-opt1-tx",
+        },
+    ),
+]
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py
new file mode 100644
index 0000000..f52a020
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains the exception classes the Mock OSM RO module can raise."""
+
+
+_PREFIX = "Mock OSM RO: "
+
+
+class MockOsmRoError(Exception):
+    """Base Exception for all Mock OSM RO related errors."""
+
+    def __init__(self, message):
+        super().__init__(_PREFIX + message)
+
+
+class MockOsmRoServiceNotFound(MockOsmRoError):
+    def __init__(self, service_id):
+        MESSAGE = "ServiceId({:s}) not found"
+        message = MESSAGE.format(str(service_id))
+        super().__init__(message)
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py
new file mode 100644
index 0000000..71c2148
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains a Mock OSM RO component that can be used for rapid unit testing.
+
+This code is based on code taken with permission from ETSI TeraFlowSDN project at:
+  https://labs.etsi.org/rep/tfs/controller
+"""
+
+
+from typing import Dict, List
+
+from osm_ro_plugin.sdnconn import SdnConnectorBase
+
+from .exceptions import MockOsmRoServiceNotFound
+
+
+class MockOsmRo:
+    def __init__(
+        self,
+        klass: SdnConnectorBase,
+        url: str,
+        wim_account: Dict,
+        wim_port_mapping: Dict,
+    ) -> None:
+        wim = {"wim_url": url}
+        config = {
+            "mapping_not_needed": False,
+            "service_endpoint_mapping": wim_port_mapping,
+        }
+
+        # Instantiate WIM connector
+        self.wim_connector = klass(wim, wim_account, config=config)
+
+        # Internal DB emulating OSM RO storage provided to WIM Connectors
+        self.conn_info = {}
+
+    def create_connectivity_service(
+        self, service_type: str, connection_points: List[Dict]
+    ) -> str:
+        self.wim_connector.check_credentials()
+        service_uuid, conn_info = self.wim_connector.create_connectivity_service(
+            service_type, connection_points
+        )
+        self.conn_info[service_uuid] = conn_info
+        return service_uuid
+
+    def get_connectivity_service_status(self, service_uuid: str) -> Dict:
+        conn_info = self.conn_info.get(service_uuid)
+        if conn_info is None:
+            raise MockOsmRoServiceNotFound(service_uuid)
+        self.wim_connector.check_credentials()
+        return self.wim_connector.get_connectivity_service_status(
+            service_uuid, conn_info=conn_info
+        )
+
+    def edit_connectivity_service(
+        self, service_uuid: str, connection_points: List[Dict]
+    ) -> None:
+        conn_info = self.conn_info.get(service_uuid)
+        if conn_info is None:
+            raise MockOsmRoServiceNotFound(service_uuid)
+        self.wim_connector.check_credentials()
+        self.wim_connector.edit_connectivity_service(
+            service_uuid, conn_info=conn_info, connection_points=connection_points
+        )
+
+    def delete_connectivity_service(self, service_uuid: str) -> None:
+        conn_info = self.conn_info.get(service_uuid)
+        if conn_info is None:
+            raise MockOsmRoServiceNotFound(service_uuid)
+        self.wim_connector.check_credentials()
+        self.wim_connector.delete_connectivity_service(
+            service_uuid, conn_info=conn_info
+        )
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py
new file mode 100644
index 0000000..5f38210
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains a minimalistic Mock Transport API (TAPI) WIM server."""
+
+import http.server
+import json
+import uuid
+
+
+PHOTONIC_PROTOCOL_QUALIFIER = "tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC"
+DSR_PROTOCOL_QUALIFIER = "tapi-dsr:DIGITAL_SIGNAL_TYPE"
+
+
+def compose_sip(
+    uuid, layer_protocol_name, supported_layer_protocol_qualifier, direction
+):
+    return {
+        "uuid": uuid,
+        "layer-protocol-name": layer_protocol_name,
+        "supported-layer-protocol-qualifier": [supported_layer_protocol_qualifier],
+        "administrative-state": "UNLOCKED",
+        "operational-state": "ENABLED",
+        "direction": direction,
+    }
+
+
+def compose_sip_dsr(uuid):
+    return compose_sip(uuid, "DSR", DSR_PROTOCOL_QUALIFIER, "BIDIRECTIONAL")
+
+
+def compose_sip_photonic_input(uuid):
+    return compose_sip(uuid, "PHOTONIC_MEDIA", PHOTONIC_PROTOCOL_QUALIFIER, "INPUT")
+
+
+def compose_sip_photonic_output(uuid):
+    return compose_sip(uuid, "PHOTONIC_MEDIA", PHOTONIC_PROTOCOL_QUALIFIER, "OUTPUT")
+
+
+CONTEXT = {
+    "uuid": str(uuid.uuid4()),
+    "service-interface-point": [
+        compose_sip_dsr("R1-eth0"),
+        compose_sip_dsr("R2-eth0"),
+        compose_sip_photonic_input("R3-opt1-rx"),
+        compose_sip_photonic_output("R3-opt1-tx"),
+        compose_sip_photonic_input("R4-opt1-rx"),
+        compose_sip_photonic_output("R4-opt1-tx"),
+    ],
+    # topology details not used by the WIM connector
+    "topology-context": {},
+    "connectivity-context": {"connectivity-service": [], "connection": []},
+}
+
+
+class MockTapiRequestHandler(http.server.BaseHTTPRequestHandler):
+    """Mock TAPI Request Handler for the unit tests"""
+
+    def do_GET(self):  # pylint: disable=invalid-name
+        """Handle GET requests"""
+        path = self.path.replace("tapi-common:", "").replace("tapi-connectivity:", "")
+
+        if path == "/restconf/data/context":
+            status = 200  # ok
+            headers = {"Content-Type": "application/json"}
+            data = CONTEXT
+        elif path == "/restconf/data/context/service-interface-point":
+            status = 200  # ok
+            headers = {"Content-Type": "application/json"}
+            data = CONTEXT["service-interface-point"]
+            data = {"tapi-common:service-interface-point": data}
+        elif path == "/restconf/data/context/connectivity-context/connectivity-service":
+            status = 200  # ok
+            headers = {"Content-Type": "application/json"}
+            data = CONTEXT["connectivity-context"]["connectivity-service"]
+            data = {"tapi-connectivity:connectivity-service": data}
+        else:
+            status = 404  # not found
+            headers = {}
+            data = {"error": "Not found"}
+
+        self.send_response(status)
+        for header_name, header_value in headers.items():
+            self.send_header(header_name, header_value)
+        self.end_headers()
+        data = json.dumps(data)
+        self.wfile.write(data.encode("UTF-8"))
+
+    def do_POST(self):  # pylint: disable=invalid-name
+        """Handle POST requests"""
+        path = self.path.replace("tapi-common:", "").replace("tapi-connectivity:", "")
+        length = int(self.headers["content-length"])
+        data = json.loads(self.rfile.read(length))
+
+        if path == "/restconf/data/context/connectivity-context":
+            if "tapi-connectivity:connectivity-service" in data:
+                data["connectivity-service"] = data.pop(
+                    "tapi-connectivity:connectivity-service"
+                )
+
+            if (
+                isinstance(data["connectivity-service"], list)
+                and len(data["connectivity-service"]) > 0
+            ):
+                data["connectivity-service"] = data["connectivity-service"][0]
+
+            conn_svc = data["connectivity-service"]
+            if "connectivity-constraint" in conn_svc:
+                conn_constr = conn_svc.pop("connectivity-constraint")
+                if "requested-capacity" in conn_constr:
+                    req_cap = conn_constr.pop("requested-capacity")
+                    conn_svc["requested-capacity"] = req_cap
+                if "connectivity-direction" in conn_constr:
+                    conn_dir = conn_constr.pop("connectivity-direction")
+                    conn_svc["connectivity-direction"] = conn_dir
+
+            connection = {"uuid": conn_svc["uuid"], "connection-end-point": []}
+            conn_svc["connection"] = [{"connection_uuid": conn_svc["uuid"]}]
+
+            CONTEXT["connectivity-context"]["connection"].append(connection)
+            CONTEXT["connectivity-context"]["connectivity-service"].append(conn_svc)
+
+            status = 201  # created
+            headers = {}
+        elif path == "/restconf/operations/delete-connectivity-service":
+            if "tapi-connectivity:input" in data:
+                data["input"] = data.pop("tapi-connectivity:input")
+            conn_svc_uuid = data["input"]["uuid"]
+            conn_ctx = CONTEXT["connectivity-context"]
+
+            # keep connectivity services and connections with different uuid
+            conn_ctx["connection"] = [
+                conn for conn in conn_ctx["connection"] if conn["uuid"] != conn_svc_uuid
+            ]
+            conn_ctx["connectivity-service"] = [
+                conn_svc
+                for conn_svc in conn_ctx["connectivity-service"]
+                if conn_svc["uuid"] != conn_svc_uuid
+            ]
+
+            status = 204  # ok, no content
+            headers = {}
+        else:
+            status = 404  # not found
+            headers = {}
+
+        self.send_response(status)
+        for header_name, header_value in headers.items():
+            self.send_header(header_name, header_value)
+        self.end_headers()
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py
new file mode 100644
index 0000000..8c18b86
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains the unit tests for the Transport API (TAPI) WIM connector."""
+
+import http.server
+import threading
+import unittest
+
+from osm_rosdn_tapi.exceptions import (
+    WimTapiConnectionPointsBadFormat,
+    WimTapiMissingConnPointField,
+    WimTapiUnsupportedServiceType,
+)
+from osm_rosdn_tapi.tests.constants import (
+    WIM_ACCOUNT,
+    WIM_HOST_PORT,
+    WIM_PORT_MAPPING,
+    WIM_URL,
+)
+from osm_rosdn_tapi.tests.mock_osm_ro import MockOsmRo
+from osm_rosdn_tapi.tests.mock_tapi_handler import MockTapiRequestHandler
+from osm_rosdn_tapi.wimconn_tapi import WimconnectorTAPI
+
+
+SERVICE_TYPE = "ELINE"
+SERVICE_CONNECTION_POINTS_BIDIRECTIONAL = [
+    # SIPs taken from mock_tapi_handler.py
+    {"service_endpoint_id": "R1-eth0"},
+    {"service_endpoint_id": "R2-eth0"},
+]
+SERVICE_CONNECTION_POINTS_UNIDIRECTIONAL = [
+    # SIPs taken from mock_tapi_handler.py
+    {"service_endpoint_id": "R3-opt1"},
+    {"service_endpoint_id": "R4-opt1"},
+]
+
+
+class UnitTests(unittest.TestCase):
+    """Unit tests for Transport API WIM connector"""
+
+    def setUp(self) -> None:
+        self.wim_server = http.server.ThreadingHTTPServer(
+            WIM_HOST_PORT, MockTapiRequestHandler
+        )
+
+    def test_wrong_cases(self):
+        with self.wim_server:
+            wim_server_thread = threading.Thread(target=self.wim_server.serve_forever)
+            wim_server_thread.daemon = True
+            wim_server_thread.start()
+
+            mock_osm_ro_tapi = MockOsmRo(
+                WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING
+            )
+
+            # Unsupported service type
+            with self.assertRaises(WimTapiUnsupportedServiceType) as test_context:
+                mock_osm_ro_tapi.create_connectivity_service(
+                    "ELAN", SERVICE_CONNECTION_POINTS_BIDIRECTIONAL
+                )
+            self.assertEqual(
+                str(test_context.exception.args[0]),
+                "Unsupported ServiceType(ELAN). Supported ServiceTypes({'ELINE'})",
+            )
+
+            # Wrong number of connection_points
+            with self.assertRaises(WimTapiConnectionPointsBadFormat) as test_context:
+                mock_osm_ro_tapi.create_connectivity_service(SERVICE_TYPE, [])
+            self.assertEqual(
+                str(test_context.exception.args[0]),
+                "ConnectionPoints([]) must be a list or tuple of length 2",
+            )
+
+            # Wrong type of connection_points
+            with self.assertRaises(WimTapiConnectionPointsBadFormat) as test_context:
+                mock_osm_ro_tapi.create_connectivity_service(
+                    SERVICE_TYPE, {"a": "b", "c": "d"}
+                )
+            self.assertEqual(
+                str(test_context.exception.args[0]),
+                "ConnectionPoints({'a': 'b', 'c': 'd'}) must be a list or tuple of length 2",
+            )
+
+            with self.assertRaises(WimTapiMissingConnPointField) as test_context:
+                mock_osm_ro_tapi.create_connectivity_service(
+                    SERVICE_TYPE,
+                    [
+                        {"wrong_service_endpoint_id": "value"},
+                        {"service_endpoint_id": "value"},
+                    ],
+                )
+            self.assertEqual(
+                str(test_context.exception.args[0]),
+                "WIM TAPI Connector: ConnectionPoint({'wrong_service_endpoint_id': 'value'}) has no field 'service_endpoint_id'",
+            )
+
+            self.wim_server.shutdown()
+            wim_server_thread.join()
+
+    def test_correct_bidirectional(self):
+        with self.wim_server:
+            wim_server_thread = threading.Thread(target=self.wim_server.serve_forever)
+            wim_server_thread.daemon = True
+            wim_server_thread.start()
+
+            mock_osm_ro_tapi = MockOsmRo(
+                WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING
+            )
+
+            # Create bidirectional TAPI service
+            service_uuid = mock_osm_ro_tapi.create_connectivity_service(
+                SERVICE_TYPE, SERVICE_CONNECTION_POINTS_BIDIRECTIONAL
+            )
+            self.assertIsInstance(service_uuid, str)
+
+            # Check status of bidirectional TAPI service
+            status = mock_osm_ro_tapi.get_connectivity_service_status(service_uuid)
+            self.assertIsInstance(status, dict)
+            self.assertIn("sdn_status", status)
+            self.assertEqual(status["sdn_status"], "ACTIVE")
+
+            # Delete bidirectional TAPI service
+            mock_osm_ro_tapi.delete_connectivity_service(service_uuid)
+
+            self.wim_server.shutdown()
+            wim_server_thread.join()
+
+    def test_correct_unidirectional(self):
+        with self.wim_server:
+            wim_server_thread = threading.Thread(target=self.wim_server.serve_forever)
+            wim_server_thread.daemon = True
+            wim_server_thread.start()
+
+            mock_osm_ro_tapi = MockOsmRo(
+                WimconnectorTAPI, WIM_URL, WIM_ACCOUNT, WIM_PORT_MAPPING
+            )
+
+            # Create unidirectional TAPI service
+            service_uuid = mock_osm_ro_tapi.create_connectivity_service(
+                SERVICE_TYPE, SERVICE_CONNECTION_POINTS_UNIDIRECTIONAL
+            )
+            self.assertIsInstance(service_uuid, str)
+
+            # Check status of unidirectional TAPI service
+            status = mock_osm_ro_tapi.get_connectivity_service_status(service_uuid)
+            self.assertIsInstance(status, dict)
+            self.assertIn("sdn_status", status)
+            self.assertEqual(status["sdn_status"], "ACTIVE")
+
+            # Delete unidirectional TAPI service
+            mock_osm_ro_tapi.delete_connectivity_service(service_uuid)
+
+            self.wim_server.shutdown()
+            wim_server_thread.join()
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py b/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py
new file mode 100644
index 0000000..b75543f
--- /dev/null
+++ b/RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+#######################################################################################
+# This file is part of OSM RO module
+#
+# Copyright ETSI Contributors and Others.
+#
+# 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.
+#######################################################################################
+# This work has been performed in the context of the TeraFlow Project -
+# funded by the European Commission under Grant number 101015857 through the
+# Horizon 2020 program.
+# Contributors:
+# - Lluis Gifre <lluis.gifre@cttc.es>
+# - Ricard Vilalta <ricard.vilalta@cttc.es>
+#######################################################################################
+
+"""This file contains a helper methods for the Mock OSM RO component that can be used
+for rapid unit testing.
+
+This code is based on code taken with permission from ETSI TeraFlowSDN project at:
+  https://labs.etsi.org/rep/tfs/controller
+"""
+
+from typing import Dict, Optional
+
+
+# Ref: https://osm.etsi.org/wikipub/index.php/WIM
+# Fields defined according to from osm_ro_plugin.sdnconn import SdnConnectorBase
+def wim_port_mapping(
+    datacenter_id: str,
+    device_id: str,
+    device_interface_id: str,
+    service_endpoint_id: str,
+    switch_dpid: Optional[str] = None,
+    switch_port: Optional[str] = None,
+    service_mapping_info: Dict = {},
+):
+    mapping = {
+        "datacenter_id": datacenter_id,
+        "device_id": device_id,
+        "device_interface_id": device_interface_id,
+        "service_endpoint_id": service_endpoint_id,
+        "service_mapping_info": service_mapping_info,
+    }
+    if switch_dpid is not None:
+        mapping["switch_dpid"] = switch_dpid
+    if switch_port is not None:
+        mapping["switch_port"] = switch_port
+    return mapping