Update from master part 2 77/13477/1
authorDario Faccin <dario.faccin@canonical.com>
Wed, 31 May 2023 12:52:06 +0000 (14:52 +0200)
committerDario Faccin <dario.faccin@canonical.com>
Wed, 31 May 2023 12:54:38 +0000 (14:54 +0200)
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>
33 files changed:
Dockerfile.local
NG-RO/osm_ng_ro/ns.py
NG-RO/osm_ng_ro/ns_thread.py
NG-RO/osm_ng_ro/tests/test_ns.py
NG-RO/osm_ng_ro/tests/test_ns_thread.py
NG-RO/osm_ng_ro/validation.py
RO-SDN-tapi/osm_rosdn_tapi/__init__.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/conn_info.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/exceptions.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/log_messages.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/message_composers.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/message_templates.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/services_composer.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/__init__.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/constants.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/exceptions.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/mock_osm_ro.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/mock_tapi_handler.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/test_wim_tapi.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/tests/tools.py [new file with mode: 0644]
RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py [new file with mode: 0644]
RO-SDN-tapi/requirements.in [new file with mode: 0644]
RO-SDN-tapi/setup.py [new file with mode: 0644]
RO-SDN-tapi/stdeb.cfg [new file with mode: 0644]
RO-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py
RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py
devops-stages/stage-build.sh
releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml [new file with mode: 0644]
releasenotes/notes/feature_10972_Support_of_volume_multi_attach-57c1232c1a54ab18.yaml [new file with mode: 0644]
requirements-dev.txt
requirements.in
tox.ini

index 36a53ee..843e053 100644 (file)
@@ -82,6 +82,9 @@ RUN python3 -m build /build/RO-SDN-juniper_contrail && \
 RUN python3 -m build /build/RO-VIM-gcp && \
     python3 -m pip install /build/RO-VIM-gcp/dist/*.whl
 
+RUN python3 -m build /build/RO-SDN-tapi && \
+    python3 -m pip install /build/RO-SDN-tapi/dist/*.whl
+
 FROM ubuntu:20.04
 
 RUN DEBIAN_FRONTEND=noninteractive apt-get --yes update && \
index fd0ad07..75bae1b 100644 (file)
@@ -117,6 +117,7 @@ class Ns(object):
             "flavor": Ns._process_flavor_params,
             "vdu": Ns._process_vdu_params,
             "affinity-or-anti-affinity-group": Ns._process_affinity_group_params,
+            "shared-volumes": Ns._process_shared_volumes_params,
         }
         self.db_path_map = {
             "net": "vld",
@@ -124,6 +125,7 @@ class Ns(object):
             "flavor": "flavor",
             "vdu": "vdur",
             "affinity-or-anti-affinity-group": "affinity-or-anti-affinity-group",
+            "shared-volumes": "shared-volumes",
         }
 
     def init_db(self, target_version):
@@ -859,7 +861,6 @@ class Ns(object):
         flavor_data_name = flavor_data.copy()
         flavor_data_name["name"] = target_flavor["name"]
         extra_dict["params"] = {"flavor_data": flavor_data_name}
-
         return extra_dict
 
     @staticmethod
@@ -1053,10 +1054,37 @@ class Ns(object):
         if not virtual_storage_desc.get("vdu-storage-requirements"):
             return False
         for item in virtual_storage_desc.get("vdu-storage-requirements", {}):
-            if item.get("key") == "keep-volume" and item.get("value") == "true":
+            if item.get("key") == "keep-volume" and item.get("value").lower() == "true":
                 return True
         return False
 
+    @staticmethod
+    def is_shared_volume(
+        virtual_storage_desc: Dict[str, Any], vnfd_id: str
+    ) -> (str, bool):
+        """Function to decide if the volume type is multi attached or not .
+
+        Args:
+            virtual_storage_desc (Dict[str, Any]): virtual storage description dictionary
+            vnfd_id (str): vnfd id
+
+        Returns:
+            bool (True/False)
+            name (str) New name if it is a multiattach disk
+        """
+
+        if vdu_storage_requirements := virtual_storage_desc.get(
+            "vdu-storage-requirements", {}
+        ):
+            for item in vdu_storage_requirements:
+                if (
+                    item.get("key") == "multiattach"
+                    and item.get("value").lower() == "true"
+                ):
+                    name = f"shared-{virtual_storage_desc['id']}-{vnfd_id}"
+                    return name, True
+        return virtual_storage_desc["id"], False
+
     @staticmethod
     def _sort_vdu_interfaces(target_vdu: dict) -> None:
         """Sort the interfaces according to position number.
@@ -1385,7 +1413,6 @@ class Ns(object):
                         "size": root_disk["size-of-storage"],
                         "keep": Ns.is_volume_keeping_required(root_disk),
                     }
-
                     disk_list.append(persistent_root_disk[vsd["id"]])
                     break
 
@@ -1395,6 +1422,7 @@ class Ns(object):
         persistent_root_disk: dict,
         persistent_ordinary_disk: dict,
         disk_list: list,
+        vnf_id: str = None,
     ) -> None:
         """Fill the disk list by adding persistent ordinary disks.
 
@@ -1412,9 +1440,12 @@ class Ns(object):
                     == "persistent-storage:persistent-storage"
                     and disk["id"] not in persistent_root_disk.keys()
                 ):
+                    name, multiattach = Ns.is_shared_volume(disk, vnf_id)
                     persistent_ordinary_disk[disk["id"]] = {
+                        "name": name,
                         "size": disk["size-of-storage"],
                         "keep": Ns.is_volume_keeping_required(disk),
+                        "multiattach": multiattach,
                     }
                     disk_list.append(persistent_ordinary_disk[disk["id"]])
 
@@ -1486,7 +1517,6 @@ class Ns(object):
         image_text = ns_preffix + ":image." + target_vdu["ns-image-id"]
         extra_dict = {"depends_on": [image_text]}
         net_list = []
-
         persistent_root_disk = {}
         persistent_ordinary_disk = {}
         vdu_instantiation_volumes_list = []
@@ -1494,7 +1524,6 @@ class Ns(object):
         disk_list = []
         vnfd_id = vnfr["vnfd-id"]
         vnfd = db.get_one("vnfds", {"_id": vnfd_id})
-
         # If the position info is provided for all the interfaces, it will be sorted
         # according to position number ascendingly.
         if all(
@@ -1565,7 +1594,11 @@ class Ns(object):
             )
             # Add the persistent non-root disks to disk_list
             Ns._add_persistent_ordinary_disks_to_disk_list(
-                target_vdu, persistent_root_disk, persistent_ordinary_disk, disk_list
+                target_vdu,
+                persistent_root_disk,
+                persistent_ordinary_disk,
+                disk_list,
+                vnfd["id"],
             )
 
         affinity_group_list = Ns._prepare_vdu_affinity_group_list(
@@ -1590,7 +1623,23 @@ class Ns(object):
             "availability_zone_index": None,  # TODO
             "availability_zone_list": None,  # TODO
         }
+        return extra_dict
 
+    @staticmethod
+    def _process_shared_volumes_params(
+        target_shared_volume: Dict[str, Any],
+        indata: Dict[str, Any],
+        vim_info: Dict[str, Any],
+        target_record_id: str,
+        **kwargs: Dict[str, Any],
+    ) -> Dict[str, Any]:
+        extra_dict = {}
+        shared_volume_data = {
+            "size": target_shared_volume["size-of-storage"],
+            "name": target_shared_volume["id"],
+            "type": target_shared_volume["type-of-storage"],
+        }
+        extra_dict["params"] = shared_volume_data
         return extra_dict
 
     @staticmethod
@@ -1628,7 +1677,6 @@ class Ns(object):
         extra_dict["params"] = {
             "affinity_group_data": affinity_group_data,
         }
-
         return extra_dict
 
     @staticmethod
@@ -1664,6 +1712,7 @@ class Ns(object):
 
         vim_details = {}
         vim_details_text = existing_vdu["vim_info"][target_id].get("vim_details", None)
+
         if vim_details_text:
             vim_details = yaml.safe_load(f"{vim_details_text}")
 
@@ -1857,7 +1906,6 @@ class Ns(object):
         process_params = None
         vdu2cloud_init = indata.get("cloud_init_content") or {}
         ro_nsr_public_key = db_ro_nsr["public_key"]
-
         # According to the type of item, the path, the target_list,
         # the existing_list and the method to process params are set
         db_path = self.db_path_map[item]
@@ -1877,27 +1925,29 @@ class Ns(object):
                 )
                 target_list = target_vnf.get(db_path, []) if target_vnf else []
                 existing_list = vnfr.get(db_path, [])
-        elif item in ("image", "flavor", "affinity-or-anti-affinity-group"):
+        elif item in (
+            "image",
+            "flavor",
+            "affinity-or-anti-affinity-group",
+            "shared-volumes",
+        ):
             db_record = "nsrs:{}:{}".format(nsr_id, db_path)
             target_list = indata.get(item, [])
             existing_list = db_nsr.get(item, [])
         else:
             raise NsException("Item not supported: {}", item)
-
         # ensure all the target_list elements has an "id". If not assign the index as id
         if target_list is None:
             target_list = []
         for target_index, tl in enumerate(target_list):
             if tl and not tl.get("id"):
                 tl["id"] = str(target_index)
-
         # step 1 items (networks,vdus,...) to be deleted/updated
         for item_index, existing_item in enumerate(existing_list):
             target_item = next(
                 (t for t in target_list if t["id"] == existing_item["id"]),
                 None,
             )
-
             for target_vim, existing_viminfo in existing_item.get(
                 "vim_info", {}
             ).items():
@@ -1941,7 +1991,6 @@ class Ns(object):
         # step 2 items (networks,vdus,...) to be created
         for target_item in target_list:
             item_index = -1
-
             for item_index, existing_item in enumerate(existing_list):
                 if existing_item["id"] == target_item["id"]:
                     break
@@ -1998,7 +2047,6 @@ class Ns(object):
                         }
                     )
                     self.logger.debug("calculate_diff_items kwargs={}".format(kwargs))
-
                 extra_dict = process_params(
                     target_item,
                     indata,
@@ -2068,7 +2116,13 @@ class Ns(object):
         changes_list = []
 
         # NS vld, image and flavor
-        for item in ["net", "image", "flavor", "affinity-or-anti-affinity-group"]:
+        for item in [
+            "net",
+            "image",
+            "flavor",
+            "affinity-or-anti-affinity-group",
+            "shared-volumes",
+        ]:
             self.logger.debug("process NS={} {}".format(nsr_id, item))
             diff_items, task_index = self.calculate_diff_items(
                 indata=indata,
index 03e8b30..03255e3 100644 (file)
@@ -347,7 +347,6 @@ class VimInteractionVdu(VimInteractionBase):
         created = False
         created_items = {}
         target_vim = self.my_vims[ro_task["target_id"]]
-
         try:
             created = True
             params = task["params"]
@@ -389,7 +388,6 @@ class VimInteractionVdu(VimInteractionBase):
                         )
 
                     affinity_group["affinity_group_id"] = affinity_group_id
-
             vim_vm_id, created_items = target_vim.new_vminstance(**params_copy)
             interfaces = [iface["vim_id"] for iface in params_copy["net_list"]]
 
@@ -691,6 +689,102 @@ class VimInteractionImage(VimInteractionBase):
             return "FAILED", ro_vim_item_update
 
 
+class VimInteractionSharedVolume(VimInteractionBase):
+    def delete(self, ro_task, task_index):
+        task = ro_task["tasks"][task_index]
+        task_id = task["task_id"]
+        shared_volume_vim_id = ro_task["vim_info"]["vim_id"]
+        ro_vim_item_update_ok = {
+            "vim_status": "DELETED",
+            "created": False,
+            "vim_message": "DELETED",
+            "vim_id": None,
+        }
+        try:
+            if shared_volume_vim_id:
+                target_vim = self.my_vims[ro_task["target_id"]]
+                target_vim.delete_shared_volumes(shared_volume_vim_id)
+        except vimconn.VimConnNotFoundException:
+            ro_vim_item_update_ok["vim_message"] = "already deleted"
+        except vimconn.VimConnException as e:
+            self.logger.error(
+                "ro_task={} vim={} del-shared-volume={}: {}".format(
+                    ro_task["_id"], ro_task["target_id"], shared_volume_vim_id, e
+                )
+            )
+            ro_vim_item_update = {
+                "vim_status": "VIM_ERROR",
+                "vim_message": "Error while deleting: {}".format(e),
+            }
+
+            return "FAILED", ro_vim_item_update
+
+        self.logger.debug(
+            "task={} {} del-shared-volume={} {}".format(
+                task_id,
+                ro_task["target_id"],
+                shared_volume_vim_id,
+                ro_vim_item_update_ok.get("vim_message", ""),
+            )
+        )
+
+        return "DONE", ro_vim_item_update_ok
+
+    def new(self, ro_task, task_index, task_depends):
+        task = ro_task["tasks"][task_index]
+        task_id = task["task_id"]
+        created = False
+        created_items = {}
+        target_vim = self.my_vims[ro_task["target_id"]]
+
+        try:
+            shared_volume_name = None
+            shared_volume_vim_id = None
+            shared_volume_data = None
+
+            if task.get("params"):
+                shared_volume_data = task["params"]
+
+            if shared_volume_data:
+                self.logger.info(
+                    f"Creating the new shared_volume for {shared_volume_data}\n"
+                )
+                (
+                    shared_volume_name,
+                    shared_volume_vim_id,
+                ) = target_vim.new_shared_volumes(shared_volume_data)
+                created = True
+                created_items[shared_volume_vim_id] = shared_volume_name
+
+            ro_vim_item_update = {
+                "vim_id": shared_volume_vim_id,
+                "vim_status": "DONE",
+                "created": created,
+                "created_items": created_items,
+                "vim_details": None,
+                "vim_message": None,
+            }
+            self.logger.debug(
+                "task={} {} new-shared-volume={} created={}".format(
+                    task_id, ro_task["target_id"], shared_volume_vim_id, created
+                )
+            )
+
+            return "DONE", ro_vim_item_update
+        except (vimconn.VimConnException, NsWorkerException) as e:
+            self.logger.error(
+                "task={} vim={} new-shared-volume:"
+                " {}".format(task_id, ro_task["target_id"], e)
+            )
+            ro_vim_item_update = {
+                "vim_status": "VIM_ERROR",
+                "created": created,
+                "vim_message": str(e),
+            }
+
+            return "FAILED", ro_vim_item_update
+
+
 class VimInteractionFlavor(VimInteractionBase):
     def delete(self, ro_task, task_index):
         task = ro_task["tasks"][task_index]
@@ -739,7 +833,6 @@ class VimInteractionFlavor(VimInteractionBase):
         created = False
         created_items = {}
         target_vim = self.my_vims[ro_task["target_id"]]
-
         try:
             # FIND
             vim_flavor_id = None
@@ -1540,6 +1633,9 @@ class NsWorker(threading.Thread):
         self.db = db
         self.item2class = {
             "net": VimInteractionNet(self.db, self.my_vims, self.db_vims, self.logger),
+            "shared-volumes": VimInteractionSharedVolume(
+                self.db, self.my_vims, self.db_vims, self.logger
+            ),
             "vdu": VimInteractionVdu(self.db, self.my_vims, self.db_vims, self.logger),
             "image": VimInteractionImage(
                 self.db, self.my_vims, self.db_vims, self.logger
@@ -2301,7 +2397,6 @@ class NsWorker(threading.Thread):
                             lock_object = LockRenew.add_lock_object(
                                 "ro_tasks", ro_task, self
                             )
-
                         if task["action"] == "DELETE":
                             (
                                 new_status,
index d966a86..a236819 100644 (file)
@@ -4547,6 +4547,8 @@ class TestProcessVduParams(unittest.TestCase):
             {
                 "size": "10",
                 "keep": False,
+                "multiattach": False,
+                "name": "persistent-volume2",
             }
         ]
         self.ns._add_persistent_ordinary_disks_to_disk_list(
index 0914a06..4e42b4f 100644 (file)
@@ -27,6 +27,7 @@ from osm_ng_ro.ns_thread import (
     VimInteractionMigration,
     VimInteractionNet,
     VimInteractionResize,
+    VimInteractionSharedVolume,
 )
 from osm_ro_plugin.vimconn import VimConnConnectionException, VimConnException
 
@@ -1450,6 +1451,218 @@ class TestVimInteractionNet(unittest.TestCase):
                 instance.refresh(ro_task)
 
 
+class TestVimInteractionSharedVolume(unittest.TestCase):
+    def setUp(self):
+        module_name = "osm_ro_plugin"
+        self.target_vim = MagicMock(name=f"{module_name}.vimconn.VimConnector")
+        self.task_depends = None
+
+        patches = [patch(f"{module_name}.vimconn.VimConnector", self.target_vim)]
+
+        # Enabling mocks and add cleanups
+        for mock in patches:
+            mock.start()
+            self.addCleanup(mock.stop)
+
+    def test__new_shared_volume_ok(self):
+        """
+        create a shared volume with attributes set in params
+        """
+        db = "test_db"
+        logger = "test_logger"
+        my_vims = "test-vim"
+        db_vims = {
+            0: {
+                "config": {},
+            },
+        }
+
+        instance = VimInteractionSharedVolume(db, logger, my_vims, db_vims)
+        with patch.object(instance, "my_vims", [self.target_vim]), patch.object(
+            instance, "logger", logging
+        ), patch.object(instance, "db_vims", db_vims):
+            ro_task = {
+                "target_id": 0,
+                "tasks": {
+                    "task_index_1": {
+                        "target_id": 0,
+                        "action_id": "123456",
+                        "nsr_id": "654321",
+                        "task_id": "123456:1",
+                        "status": "SCHEDULED",
+                        "action": "CREATE",
+                        "item": "test_item",
+                        "target_record": "test_target_record",
+                        "target_record_id": "test_target_record_id",
+                        # values coming from extra_dict
+                        "params": {
+                            "shared_volume_data": {
+                                "size": "10",
+                                "name": "shared-volume",
+                                "type": "multiattach",
+                            }
+                        },
+                        "find_params": {},
+                        "depends_on": "test_depends_on",
+                    },
+                },
+            }
+            task_index = "task_index_1"
+            self.target_vim.new_shared_volumes.return_value = ("", "shared-volume")
+            result = instance.new(ro_task, task_index, self.task_depends)
+            self.assertEqual(result[0], "DONE")
+            self.assertEqual(result[1].get("vim_id"), "shared-volume")
+            self.assertEqual(result[1].get("created"), True)
+            self.assertEqual(result[1].get("vim_status"), "DONE")
+
+    def test__new_shared_volume_failed(self):
+        """
+        create a shared volume with attributes set in params failed
+        """
+        db = "test_db"
+        logger = "test_logger"
+        my_vims = "test-vim"
+        db_vims = {
+            0: {
+                "config": {},
+            },
+        }
+
+        instance = VimInteractionSharedVolume(db, logger, my_vims, db_vims)
+        with patch.object(instance, "my_vims", [self.target_vim]), patch.object(
+            instance, "logger", logging
+        ), patch.object(instance, "db_vims", db_vims):
+            ro_task = {
+                "target_id": 0,
+                "tasks": {
+                    "task_index_1": {
+                        "target_id": 0,
+                        "action_id": "123456",
+                        "nsr_id": "654321",
+                        "task_id": "123456:1",
+                        "status": "SCHEDULED",
+                        "action": "CREATE",
+                        "item": "test_item",
+                        "target_record": "test_target_record",
+                        "target_record_id": "test_target_record_id",
+                        # values coming from extra_dict
+                        "params": {
+                            "shared_volume_data": {
+                                "size": "10",
+                                "name": "shared-volume",
+                                "type": "multiattach",
+                            }
+                        },
+                        "find_params": {},
+                        "depends_on": "test_depends_on",
+                    },
+                },
+            }
+            task_index = "task_index_1"
+            self.target_vim.new_shared_volumes.side_effect = VimConnException(
+                "Connection failed."
+            )
+            result = instance.new(ro_task, task_index, self.task_depends)
+            self.assertEqual(result[0], "FAILED")
+            self.assertEqual(result[1].get("vim_message"), "Connection failed.")
+            self.assertEqual(result[1].get("created"), False)
+            self.assertEqual(result[1].get("vim_status"), "VIM_ERROR")
+
+    def test__delete_shared_volume_ok(self):
+        """
+        Delete a shared volume with attributes set in params
+        """
+        db = "test_db"
+        logger = "test_logger"
+        my_vims = "test-vim"
+        db_vims = {
+            0: {
+                "config": {},
+            },
+        }
+
+        instance = VimInteractionSharedVolume(db, logger, my_vims, db_vims)
+        with patch.object(instance, "my_vims", [self.target_vim]), patch.object(
+            instance, "logger", logging
+        ), patch.object(instance, "db_vims", db_vims):
+            ro_task = {
+                "target_id": 0,
+                "tasks": {
+                    "task_index_3": {
+                        "target_id": 0,
+                        "task_id": "123456:1",
+                    },
+                },
+                "vim_info": {
+                    "created": False,
+                    "created_items": None,
+                    "vim_id": "sample_shared_volume_id_3",
+                    "vim_name": "sample_shared_volume_3",
+                    "vim_status": None,
+                    "vim_details": "some-details",
+                    "vim_message": None,
+                    "refresh_at": None,
+                },
+            }
+
+            task_index = "task_index_3"
+            self.target_vim.delete_shared_volumes.return_value = True
+            result = instance.delete(ro_task, task_index)
+            self.assertEqual(result[0], "DONE")
+            self.assertEqual(result[1].get("vim_id"), None)
+            self.assertEqual(result[1].get("created"), False)
+            self.assertEqual(result[1].get("vim_status"), "DELETED")
+
+    def test__delete_shared_volume_failed(self):
+        """
+        Delete a shared volume with attributes set in params failed
+        """
+        db = "test_db"
+        logger = "test_logger"
+        my_vims = "test-vim"
+        db_vims = {
+            0: {
+                "config": {},
+            },
+        }
+
+        instance = VimInteractionSharedVolume(db, logger, my_vims, db_vims)
+        with patch.object(instance, "my_vims", [self.target_vim]), patch.object(
+            instance, "logger", logging
+        ), patch.object(instance, "db_vims", db_vims):
+            ro_task = {
+                "_id": "122436:1",
+                "target_id": 0,
+                "tasks": {
+                    "task_index_3": {
+                        "target_id": 0,
+                        "task_id": "123456:1",
+                    },
+                },
+                "vim_info": {
+                    "created": False,
+                    "created_items": None,
+                    "vim_id": "sample_shared_volume_id_3",
+                    "vim_name": "sample_shared_volume_3",
+                    "vim_status": None,
+                    "vim_details": "some-details",
+                    "vim_message": None,
+                    "refresh_at": None,
+                },
+            }
+
+            task_index = "task_index_3"
+            self.target_vim.delete_shared_volumes.side_effect = VimConnException(
+                "Connection failed."
+            )
+            result = instance.delete(ro_task, task_index)
+            self.assertEqual(result[0], "FAILED")
+            self.assertEqual(
+                result[1].get("vim_message"), "Error while deleting: Connection failed."
+            )
+            self.assertEqual(result[1].get("vim_status"), "VIM_ERROR")
+
+
 class TestVimInteractionAffinityGroup(unittest.TestCase):
     def setUp(self):
         module_name = "osm_ro_plugin"
index 2601e90..e4eed74 100644 (file)
@@ -101,6 +101,7 @@ deploy_schema = {
         },
         "image": deploy_item_list,
         "flavor": deploy_item_list,
+        "shared-volumes": deploy_item_list,
         "ns": {
             "type": "object",
             "properties": {
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/__init__.py b/RO-SDN-tapi/osm_rosdn_tapi/__init__.py
new file mode 100644 (file)
index 0000000..ab3006a
--- /dev/null
@@ -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/conn_info.py b/RO-SDN-tapi/osm_rosdn_tapi/conn_info.py
new file mode 100644 (file)
index 0000000..70c6e59
--- /dev/null
@@ -0,0 +1,77 @@
+# -*- 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 methods to compose the conn_info data structures for the
+Transport API (TAPI) WIM connector."""
+
+
+def conn_info_compose_unidirectional(
+    service_az_uuid,
+    service_az_endpoints,
+    service_za_uuid,
+    service_za_endpoints,
+    requested_capacity=None,
+    vlan_constraint=None,
+):
+    conn_info_az = {
+        "uuid": service_az_uuid,
+        "endpoints": service_az_endpoints,
+    }
+    conn_info_za = {
+        "uuid": service_za_uuid,
+        "endpoints": service_za_endpoints,
+    }
+    if requested_capacity is not None:
+        conn_info_az["requested_capacity"] = requested_capacity
+        conn_info_za["requested_capacity"] = requested_capacity
+    if vlan_constraint is not None:
+        conn_info_az["vlan_constraint"] = vlan_constraint
+        conn_info_za["vlan_constraint"] = vlan_constraint
+    conn_info = {
+        "az": conn_info_az,
+        "za": conn_info_za,
+        "bidirectional": False,
+    }
+    return conn_info
+
+
+def conn_info_compose_bidirectional(
+    service_uuid,
+    service_endpoints,
+    requested_capacity=None,
+    vlan_constraint=None,
+):
+    conn_info = {
+        "uuid": service_uuid,
+        "endpoints": service_endpoints,
+        "bidirectional": True,
+    }
+    if requested_capacity is not None:
+        conn_info["requested_capacity"] = requested_capacity
+    if vlan_constraint is not None:
+        conn_info["vlan_constraint"] = vlan_constraint
+    return conn_info
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/exceptions.py b/RO-SDN-tapi/osm_rosdn_tapi/exceptions.py
new file mode 100644 (file)
index 0000000..6634422
--- /dev/null
@@ -0,0 +1,128 @@
+# -*- 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 Transport API (TAPI) WIM connector
+can raise in case of error."""
+
+
+from http import HTTPStatus
+
+from osm_ro_plugin.sdnconn import SdnConnectorError
+
+from .log_messages import (
+    _PREFIX,
+)
+
+
+class WimTapiError(SdnConnectorError):
+    """Base Exception for all WIM TAPI related errors."""
+
+    def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value):
+        super().__init__(_PREFIX + message)
+        self.http_code = http_code
+
+
+class WimTapiConnectionPointsBadFormat(SdnConnectorError):
+    def __init__(self, connection_points):
+        MESSAGE = "ConnectionPoints({:s}) must be a list or tuple of length 2"
+        message = MESSAGE.format(str(connection_points))
+        super().__init__(message, http_code=HTTPStatus.BAD_REQUEST)
+
+
+class WimTapiIncongruentDirectionality(WimTapiError):
+    def __init__(self, services, service_endpoint_id):
+        MESSAGE = "Incongruent directionality: services={:s} service_endpoint_id={:s}"
+        message = MESSAGE.format(str(services), str(service_endpoint_id))
+        super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+
+class WimTapiIncongruentEndPoints(WimTapiError):
+    def __init__(self, services, service_endpoint_id):
+        MESSAGE = "Incongruent endpoints: services={:s} service_endpoint_id={:s}"
+        message = MESSAGE.format(str(services), str(service_endpoint_id))
+        super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+
+class WimTapiMissingConnPointField(WimTapiError):
+    def __init__(self, connection_point, field_name):
+        MESSAGE = "ConnectionPoint({:s}) has no field '{:s}'"
+        message = MESSAGE.format(str(connection_point), str(field_name))
+        super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+
+class WimTapiMissingMappingField(WimTapiError):
+    def __init__(self, mapping, field_name):
+        MESSAGE = "Mapping({:s}) has no field '{:s}'"
+        message = MESSAGE.format(str(mapping), str(field_name))
+        super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+
+class WimTapiServerNotAvailable(WimTapiError):
+    def __init__(self, message):
+        message = "Server not available: " + message
+        super().__init__(message, http_code=HTTPStatus.SERVICE_UNAVAILABLE)
+
+
+class WimTapiServerRequestFailed(WimTapiError):
+    def __init__(self, message, http_code):
+        message = "Server request failed: " + message
+        super().__init__(message, http_code=http_code)
+
+
+class WimTapiSipNotFound(WimTapiError):
+    def __init__(self, sip_id, sips):
+        MESSAGE = "SIP({:s}) not found in context SIPs({:s})"
+        message = MESSAGE.format(str(sip_id), str(sips))
+        super().__init__(message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+
+class WimTapiConnectivityServiceCreateFailed(WimTapiError):
+    def __init__(self, name, service_id, status_code, reply):
+        MESSAGE = "Create ConnectivityService({:s}, {:s}) Failed: reply={:s}"
+        message = MESSAGE.format(str(name), str(service_id), str(reply))
+        super().__init__(message, http_code=status_code)
+
+
+class WimTapiConnectivityServiceGetStatusFailed(WimTapiError):
+    def __init__(self, name, service_id, status_code, reply):
+        MESSAGE = "Get Status of ConnectivityService({:s}, {:s}) Failed: reply={:s}"
+        message = MESSAGE.format(str(name), str(service_id), str(reply))
+        super().__init__(message, http_code=status_code)
+
+
+class WimTapiConnectivityServiceDeleteFailed(WimTapiError):
+    def __init__(self, name, service_id, status_code, reply):
+        MESSAGE = "Delete ConnectivityService({:s}, {:s}) Failed: reply={:s}"
+        message = MESSAGE.format(str(name), str(service_id), str(reply))
+        super().__init__(message, http_code=status_code)
+
+
+class WimTapiUnsupportedServiceType(SdnConnectorError):
+    def __init__(self, service_type, supported_service_types):
+        MESSAGE = "Unsupported ServiceType({:s}). Supported ServiceTypes({:s})"
+        message = MESSAGE.format(str(service_type), str(supported_service_types))
+        super().__init__(message, http_code=HTTPStatus.BAD_REQUEST)
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/log_messages.py b/RO-SDN-tapi/osm_rosdn_tapi/log_messages.py
new file mode 100644 (file)
index 0000000..f8fc02a
--- /dev/null
@@ -0,0 +1,59 @@
+# -*- 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 templete strings used to generate log messages for the
+Transport API (TAPI) WIM connector."""
+
+_PREFIX = "WIM TAPI Connector: "
+
+
+LOG_MSG_CREATE_REQUEST = (
+    _PREFIX + "Create Connectivity Service: Request {:s} {:s}: {:s}"
+)
+
+LOG_MSG_CREATE_REPLY = (
+    _PREFIX
+    + "Create Connectivity Service: Reply {:s} {:s}: status_code={:d} reply={:s}"
+)
+
+LOG_MSG_GET_STATUS_REQUEST = (
+    _PREFIX + "Get Connectivity Service Status: Request {:s} {:s}"
+)
+
+LOG_MSG_GET_STATUS_REPLY = (
+    _PREFIX
+    + "Get Connectivity Service Status: Reply {:s} {:s}: status_code={:d} reply={:s}"
+)
+
+LOG_MSG_DELETE_REQUEST = (
+    _PREFIX + "Delete Connectivity Service: Request {:s} {:s}: {:s}"
+)
+
+LOG_MSG_DELETE_REPLY = (
+    _PREFIX
+    + "Delete Connectivity Service: Reply {:s} {:s}: status_code={:d} reply={:s}"
+)
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/message_composers.py b/RO-SDN-tapi/osm_rosdn_tapi/message_composers.py
new file mode 100644 (file)
index 0000000..f5bcdae
--- /dev/null
@@ -0,0 +1,93 @@
+# -*- 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 helper methods used to compose the Transport API (TAPI)
+messages sent by the TAPI WIM connector to the WIM."""
+
+
+import copy
+
+from .message_templates import (
+    CREATE_TEMPLATE,
+    DELETE_TEMPLATE,
+    ENDPOINT_TEMPLATE,
+    REQUESTED_CAPACITY_TEMPLATE,
+    VLAN_CONSTRAINT_TEMPLATE,
+)
+
+
+def compose_requested_capacity(capacity, unit="GBPS"):
+    requested_capacity = copy.deepcopy(REQUESTED_CAPACITY_TEMPLATE)
+    total_size = requested_capacity["total-size"]
+    total_size["value"] = capacity
+    total_size["unit"] = "GBPS"
+    return requested_capacity
+
+
+def compose_vlan_constraint(vlan_id):
+    vlan_constraint = copy.deepcopy(VLAN_CONSTRAINT_TEMPLATE)
+    vlan_constraint["vlan-id"] = vlan_id
+    return vlan_constraint
+
+
+def compose_endpoint(sip):
+    sip_uuid = sip["uuid"]
+    endpoint = copy.deepcopy(ENDPOINT_TEMPLATE)
+    endpoint["service-interface-point"]["service-interface-point-uuid"] = sip_uuid
+    endpoint["layer-protocol-name"] = sip["layer-protocol-name"]
+    # TODO: implement smart selection of layer-protocol-qualifier instead of selecting first one available
+    supported_layer_protocol_qualifier = sip["supported-layer-protocol-qualifier"][0]
+    endpoint["layer-protocol-qualifier"] = supported_layer_protocol_qualifier
+    endpoint["local-id"] = sip_uuid
+    return endpoint
+
+
+def compose_create_request(
+    service_uuid,
+    endpoints,
+    bidirectional=False,
+    requested_capacity=None,
+    vlan_constraint=None,
+):
+    request = copy.deepcopy(CREATE_TEMPLATE)
+    con_svc = request["tapi-connectivity:connectivity-service"][0]
+    con_svc["uuid"] = service_uuid
+    con_svc["connectivity-direction"] = (
+        "BIDIRECTIONAL" if bidirectional else "UNIDIRECTIONAL"
+    )
+    con_svc["end-point"] = endpoints
+    if requested_capacity is not None:
+        con_svc["requested-capacity"] = requested_capacity
+    if vlan_constraint is not None:
+        con_svc["vlan-constraint"] = vlan_constraint
+    return request
+
+
+def compose_delete_request(service_uuid):
+    request = copy.deepcopy(DELETE_TEMPLATE)
+    request["tapi-connectivity:input"]["uuid"] = service_uuid
+    return request
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/message_templates.py b/RO-SDN-tapi/osm_rosdn_tapi/message_templates.py
new file mode 100644 (file)
index 0000000..ec2f0a1
--- /dev/null
@@ -0,0 +1,54 @@
+# -*- 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 template JSON-encoded messages used to compose the Transport
+API (TAPI) messages sent by the TAPI WIM connector to the WIM."""
+
+REQUESTED_CAPACITY_TEMPLATE = {"total-size": {"value": None, "unit": "GBPS"}}
+
+VLAN_CONSTRAINT_TEMPLATE = {"vlan-id": None}
+
+ENDPOINT_TEMPLATE = {
+    "service-interface-point": {"service-interface-point-uuid": None},
+    "layer-protocol-name": None,
+    "layer-protocol-qualifier": None,
+    "local-id": None,
+}
+
+CREATE_TEMPLATE = {
+    "tapi-connectivity:connectivity-service": [
+        {
+            "uuid": None,
+            # "requested-capacity": REQUESTED_CAPACITY_TEMPLATE,
+            "connectivity-direction": "UNIDIRECTIONAL",
+            "end-point": [],
+            # "vlan-constraint": VLAN_CONSTRAINT_TEMPLATE,
+        }
+    ]
+}
+
+DELETE_TEMPLATE = {"tapi-connectivity:input": {"uuid": None}}
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/services_composer.py b/RO-SDN-tapi/osm_rosdn_tapi/services_composer.py
new file mode 100644 (file)
index 0000000..720e423
--- /dev/null
@@ -0,0 +1,151 @@
+# -*- 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 ServiceComposer class used by the Transport API (TAPI) WIM
+connector to compose the services based on the service_endpoint_ids and their
+directionality."""
+
+from .exceptions import (
+    WimTapiIncongruentDirectionality,
+    WimTapiIncongruentEndPoints,
+    WimTapiMissingMappingField,
+    WimTapiSipNotFound,
+)
+from .message_composers import (
+    compose_endpoint,
+    compose_requested_capacity,
+    # compose_vlan_constraint,
+)
+
+
+class ServicesComposer:
+    def __init__(self, service_interface_points) -> None:
+        self.sips = service_interface_points
+
+        # if unidirectional
+        #   - a single service_endpoint item is created
+        #   - the service_endpoint item contains with the 2 bidirectional SIPs
+        # if bidirectional
+        #   - two service_endpoint items are created
+        #   - each service_endpoint item containing a list of 2 unidirectional SIPs (in, out)
+        self.services = list()
+
+        # TODO: populate dynamically capacity of the connection
+        self.requested_capacity = compose_requested_capacity(1, unit="GBPS")
+
+        self.vlan_constraint = None
+        # TODO: VLAN needs to be processed by connection point; by now deactivated
+        # if connection_point.get("service_endpoint_encapsulation_type") == "dot1q":
+        #    encap_info = connection_point.get("service_endpoint_encapsulation_info", {})
+        #    vlan_id = encap_info.get("vlan")
+        #    if vlan_id is not None:
+        #        vlan_constraint = compose_vlan_constraint(vlan_id)
+
+    def add_bidirectional(self, service_endpoint_id):
+        if len(self.services) == 0:
+            # assume bidirectional, SIP is service_endpoint_id
+            service_interface_point = self.sips[service_endpoint_id]
+            self.services.append([compose_endpoint(service_interface_point)])
+        elif len(self.services) == 1:
+            # is bidirectional, SIP is service_endpoint_id
+            if len(self.services[0]) > 1:
+                # too much endpoints per service
+                raise WimTapiIncongruentEndPoints(self.services, service_endpoint_id)
+            self.services[0].append(compose_endpoint(self.sips[service_endpoint_id]))
+        else:
+            raise WimTapiIncongruentDirectionality(self.services, service_endpoint_id)
+
+    def add_unidirectional(self, service_endpoint_id, sip_input, sip_output):
+        if len(self.services) == 0:
+            # assume unidirectional
+            self.services.append([compose_endpoint(self.sips[sip_output])])  # AZ
+            self.services.append([compose_endpoint(self.sips[sip_input])])  # ZA
+        elif len(self.services) == 2:
+            # is unidirectional
+
+            if len(self.services[0]) > 1:
+                # too much endpoints per service
+                raise WimTapiIncongruentEndPoints(self.services[0], service_endpoint_id)
+            self.services[0].append(compose_endpoint(self.sips[sip_input]))  # AZ
+
+            if len(self.services[1]) > 1:
+                # too much endpoints per service
+                raise WimTapiIncongruentEndPoints(self.services[1], service_endpoint_id)
+            self.services[1].insert(0, compose_endpoint(self.sips[sip_output]))  # ZA
+        else:
+            raise WimTapiIncongruentDirectionality(self.services, service_endpoint_id)
+
+    def add_service_endpoint(self, service_endpoint_id, mapping):
+        service_mapping_info = mapping.get("service_mapping_info", {})
+
+        if (
+            len(service_mapping_info) == 0
+            or "sip_input" not in service_mapping_info
+            or "sip_output" not in service_mapping_info
+        ):
+            # bidirectional (no mapping or no sip_input or no sip_output)
+            if service_endpoint_id not in self.sips:
+                raise WimTapiSipNotFound(service_endpoint_id, self.sips)
+            self.add_bidirectional(service_endpoint_id)
+
+        else:
+            # unidirectional, sip_input and sip_output provided in mapping
+
+            sip_input = service_mapping_info.get("sip_input")
+            if sip_input is None:
+                raise WimTapiMissingMappingField(
+                    mapping, "service_mapping_info.sip_input"
+                )
+
+            if sip_input not in self.sips:
+                raise WimTapiSipNotFound(sip_input, self.sips)
+
+            sip_output = service_mapping_info.get("sip_output")
+            if sip_output is None:
+                raise WimTapiMissingMappingField(
+                    mapping, "service_mapping_info.sip_output"
+                )
+
+            if sip_output not in self.sips:
+                raise WimTapiSipNotFound(sip_output, self.sips)
+
+            self.add_unidirectional(service_endpoint_id, sip_input, sip_output)
+
+    def is_bidirectional(self):
+        return len(self.services) == 1
+
+    def dump(self, logger):
+        str_data = "\n".join(
+            [
+                "services_composer {",
+                "  services={:s}".format(str(self.services)),
+                "  requested_capacity={:s}".format(str(self.requested_capacity)),
+                "  vlan_constraint={:s}".format(str(self.vlan_constraint)),
+                "}",
+            ]
+        )
+        logger.debug(str_data)
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py b/RO-SDN-tapi/osm_rosdn_tapi/tapi_client.py
new file mode 100644 (file)
index 0000000..ad5f9f3
--- /dev/null
@@ -0,0 +1,240 @@
+# -*- 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 TransportApiClient class used by the Transport API
+(TAPI) WIM connector to interact with the underlying WIM."""
+
+import requests
+
+from .exceptions import (
+    WimTapiConnectivityServiceCreateFailed,
+    WimTapiConnectivityServiceDeleteFailed,
+    WimTapiConnectivityServiceGetStatusFailed,
+    WimTapiServerNotAvailable,
+    WimTapiServerRequestFailed,
+)
+from .log_messages import (
+    LOG_MSG_CREATE_REPLY,
+    LOG_MSG_CREATE_REQUEST,
+    LOG_MSG_DELETE_REPLY,
+    LOG_MSG_DELETE_REQUEST,
+    LOG_MSG_GET_STATUS_REPLY,
+    LOG_MSG_GET_STATUS_REQUEST,
+)
+from .message_composers import (
+    compose_create_request,
+    compose_delete_request,
+)
+
+DEFAULT_TIMEOUT = 30
+
+SUCCESS_HTTP_CODES = {
+    requests.codes.ok,  # pylint: disable=no-member
+    requests.codes.created,  # pylint: disable=no-member
+    requests.codes.accepted,  # pylint: disable=no-member
+    requests.codes.no_content,  # pylint: disable=no-member
+}
+
+RESTCONF_DATA_URL = "{:s}/restconf/data"
+RESTCONF_OPER_URL = "{:s}/restconf/operations"
+
+CONTEXT_URL = RESTCONF_DATA_URL + "/tapi-common:context"
+CTX_SIPS_URL = CONTEXT_URL + "/service-interface-point"
+CONN_CTX_URL = CONTEXT_URL + "/tapi-connectivity:connectivity-context"
+CONN_SVC_URL = CONN_CTX_URL + "/connectivity-service"
+DELETE_URL = RESTCONF_OPER_URL + "/tapi-connectivity:delete-connectivity-service"
+
+
+class TransportApiClient:
+    def __init__(self, logger, wim, wim_account, config) -> None:
+        self.logger = logger
+        self.wim_url = wim["wim_url"]
+
+        user = wim_account.get("user")
+        password = wim_account.get("password")
+        self.auth = (
+            None
+            if user is None or user == "" or password is None or password == ""
+            else (user, password)
+        )
+
+        self.headers = {"Content-Type": "application/json"}
+        self.timeout = int(config.get("timeout", DEFAULT_TIMEOUT))
+
+    def get_root_context(self):
+        context_url = CONTEXT_URL.format(self.wim_url)
+
+        try:
+            response = requests.get(
+                context_url, auth=self.auth, headers=self.headers, timeout=self.timeout
+            )
+            http_code = response.status_code
+        except requests.exceptions.RequestException as e:
+            raise WimTapiServerNotAvailable(str(e))
+
+        if http_code != 200:
+            raise WimTapiServerRequestFailed(
+                "Unexpected status code", http_code=http_code
+            )
+
+        return response.json()
+
+    def get_service_interface_points(self):
+        get_sips_url = CTX_SIPS_URL.format(self.wim_url)
+
+        try:
+            response = requests.get(
+                get_sips_url, auth=self.auth, headers=self.headers, timeout=self.timeout
+            )
+            http_code = response.status_code
+        except requests.exceptions.RequestException as e:
+            raise WimTapiServerNotAvailable(str(e))
+
+        if http_code != 200:
+            raise WimTapiServerRequestFailed(
+                "Unexpected status code", http_code=http_code
+            )
+
+        response = response.json()
+        response = response.get("tapi-common:service-interface-point", [])
+        return {sip["uuid"]: sip for sip in response}
+
+    def get_service_status(self, name, service_uuid):
+        self.logger.debug(LOG_MSG_GET_STATUS_REQUEST.format(name, service_uuid))
+
+        try:
+            services_url = CONN_SVC_URL.format(self.wim_url)
+            response = requests.get(
+                services_url, auth=self.auth, headers=self.headers, timeout=self.timeout
+            )
+            self.logger.debug(
+                LOG_MSG_GET_STATUS_REPLY.format(
+                    name, service_uuid, response.status_code, response.text
+                )
+            )
+        except requests.exceptions.ConnectionError as e:
+            status_code = e.response.status_code if e.response is not None else 500
+            content = e.response.text if e.response is not None else ""
+            raise WimTapiConnectivityServiceGetStatusFailed(
+                name, service_uuid, status_code, content
+            )
+
+        if response.status_code not in SUCCESS_HTTP_CODES:
+            raise WimTapiConnectivityServiceGetStatusFailed(
+                name, service_uuid, response.status_code, response.text
+            )
+
+        json_response = response.json()
+        connectivity_services = json_response.get(
+            "tapi-connectivity:connectivity-service", []
+        )
+        connectivity_service = next(
+            iter(
+                [
+                    connectivity_service
+                    for connectivity_service in connectivity_services
+                    if connectivity_service.get("uuid") == service_uuid
+                ]
+            ),
+            None,
+        )
+
+        if connectivity_service is None:
+            service_status = {"sdn_status": "ERROR"}
+        else:
+            service_status = {"sdn_status": "ACTIVE"}
+        return service_status
+
+    def create_service(
+        self,
+        name,
+        service_uuid,
+        service_endpoints,
+        bidirectional=False,
+        requested_capacity=None,
+        vlan_constraint=None,
+    ):
+        request_create = compose_create_request(
+            service_uuid,
+            service_endpoints,
+            bidirectional=bidirectional,
+            requested_capacity=requested_capacity,
+            vlan_constraint=vlan_constraint,
+        )
+        self.logger.debug(
+            LOG_MSG_CREATE_REQUEST.format(name, service_uuid, str(request_create))
+        )
+
+        try:
+            create_url = CONN_CTX_URL.format(self.wim_url)
+            response = requests.post(
+                create_url, headers=self.headers, json=request_create, auth=self.auth
+            )
+            self.logger.debug(
+                LOG_MSG_CREATE_REPLY.format(
+                    name, service_uuid, response.status_code, response.text
+                )
+            )
+        except requests.exceptions.ConnectionError as e:
+            status_code = e.response.status_code if e.response is not None else 500
+            content = e.response.text if e.response is not None else ""
+            raise WimTapiConnectivityServiceCreateFailed(
+                name, service_uuid, status_code, content
+            )
+
+        if response.status_code not in SUCCESS_HTTP_CODES:
+            raise WimTapiConnectivityServiceCreateFailed(
+                name, service_uuid, response.status_code, response.text
+            )
+
+    def delete_service(self, name, service_uuid):
+        request_delete = compose_delete_request(service_uuid)
+        self.logger.debug(
+            LOG_MSG_DELETE_REQUEST.format(name, service_uuid, str(request_delete))
+        )
+
+        try:
+            delete_url = DELETE_URL.format(self.wim_url)
+            response = requests.post(
+                delete_url, headers=self.headers, json=request_delete, auth=self.auth
+            )
+            self.logger.debug(
+                LOG_MSG_DELETE_REPLY.format(
+                    name, service_uuid, response.status_code, response.text
+                )
+            )
+        except requests.exceptions.ConnectionError as e:
+            status_code = e.response.status_code if e.response is not None else 500
+            content = e.response.text if e.response is not None else ""
+            raise WimTapiConnectivityServiceDeleteFailed(
+                name, service_uuid, status_code, content
+            )
+
+        if response.status_code not in SUCCESS_HTTP_CODES:
+            raise WimTapiConnectivityServiceDeleteFailed(
+                name, service_uuid, response.status_code, response.text
+            )
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 (file)
index 0000000..ab3006a
--- /dev/null
@@ -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 (file)
index 0000000..a9808ba
--- /dev/null
@@ -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 (file)
index 0000000..f52a020
--- /dev/null
@@ -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 (file)
index 0000000..71c2148
--- /dev/null
@@ -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 (file)
index 0000000..5f38210
--- /dev/null
@@ -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 (file)
index 0000000..8c18b86
--- /dev/null
@@ -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 (file)
index 0000000..b75543f
--- /dev/null
@@ -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
diff --git a/RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py b/RO-SDN-tapi/osm_rosdn_tapi/wimconn_tapi.py
new file mode 100644 (file)
index 0000000..a89f4c8
--- /dev/null
@@ -0,0 +1,346 @@
+# -*- 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>
+#######################################################################################
+
+"""The SDN/WIM connector is responsible for establishing wide area network
+connectivity.
+
+This SDN/WIM connector implements the standard ONF Transport API (TAPI).
+
+It receives the endpoints and the necessary details to request the Layer 2
+service through the use of the ONF Transport API.
+"""
+
+import logging
+import uuid
+
+from osm_ro_plugin.sdnconn import SdnConnectorBase
+
+from .conn_info import (
+    conn_info_compose_bidirectional,
+    conn_info_compose_unidirectional,
+)
+from .exceptions import (
+    WimTapiConnectionPointsBadFormat,
+    WimTapiMissingConnPointField,
+    WimTapiUnsupportedServiceType,
+)
+from .services_composer import ServicesComposer
+from .tapi_client import TransportApiClient
+
+
+class WimconnectorTAPI(SdnConnectorBase):
+    """ONF TAPI WIM connector"""
+
+    def __init__(self, wim, wim_account, config=None, logger=None):
+        """ONF TAPI WIM connector
+
+        Arguments:
+            wim (dict): WIM record, as stored in the database
+            wim_account (dict): WIM account record, as stored in the database
+            config (optional dict): optional configuration from the configuration database
+            logger (optional Logger): logger to use with this WIM connector
+        The arguments of the constructor are converted to object attributes.
+        An extra property, ``service_endpoint_mapping`` is created from ``config``.
+        """
+        logger = logger or logging.getLogger("ro.sdn.tapi")
+
+        super().__init__(wim, wim_account, config, logger)
+
+        self.logger.debug("self.config={:s}".format(str(self.config)))
+
+        if len(self.service_endpoint_mapping) == 0 and self.config.get(
+            "wim_port_mapping"
+        ):
+            self.service_endpoint_mapping = self.config.get("wim_port_mapping", [])
+
+        self.mappings = {
+            m["service_endpoint_id"]: m for m in self.service_endpoint_mapping
+        }
+
+        self.logger.debug("self.mappings={:s}".format(str(self.mappings)))
+
+        self.tapi_client = TransportApiClient(self.logger, wim, wim_account, config)
+
+        self.logger.info("TAPI WIM Connector Initialized.")
+
+    def check_credentials(self):
+        """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url),
+            user (wim_account.user), and password (wim_account.password)
+
+        Raises:
+            SdnConnectorError: Issues regarding authorization, access to
+                external URLs, etc are detected.
+        """
+        _ = self.tapi_client.get_root_context()
+        self.logger.info("Credentials checked")
+
+    def get_connectivity_service_status(self, service_uuid, conn_info=None):
+        """Monitor the status of the connectivity service established
+
+        Arguments:
+            service_uuid (str): UUID of the connectivity service
+            conn_info (dict or None): Information returned by the connector
+                during the service creation/edition and subsequently stored in
+                the database.
+
+        Returns:
+            dict: JSON/YAML-serializable dict that contains a mandatory key
+                ``sdn_status`` associated with one of the following values::
+
+                    {'sdn_status': 'ACTIVE'}
+                        # The service is up and running.
+
+                    {'sdn_status': 'INACTIVE'}
+                        # The service was created, but the connector
+                        # cannot determine yet if connectivity exists
+                        # (ideally, the caller needs to wait and check again).
+
+                    {'sdn_status': 'DOWN'}
+                        # Connection was previously established,
+                        # but an error/failure was detected.
+
+                    {'sdn_status': 'ERROR'}
+                        # An error occurred when trying to create the service/
+                        # establish the connectivity.
+
+                    {'sdn_status': 'BUILD'}
+                        # Still trying to create the service, the caller
+                        # needs to wait and check again.
+
+                Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**)
+                keys can be used to provide additional status explanation or
+                new information available for the connectivity service.
+        """
+        sdn_status = set()
+        bidirectional = conn_info["bidirectional"]
+
+        tapi_client = self.tapi_client
+        if bidirectional:
+            service_uuid = conn_info["uuid"]
+            service_status = tapi_client.get_service_status("<>", service_uuid)
+            sdn_status.add(service_status["sdn_status"])
+        else:
+            service_az_uuid = conn_info["az"]["uuid"]
+            service_za_uuid = conn_info["za"]["uuid"]
+            service_az_status = tapi_client.get_service_status(">>", service_az_uuid)
+            service_za_status = tapi_client.get_service_status("<<", service_za_uuid)
+            sdn_status.add(service_az_status["sdn_status"])
+            sdn_status.add(service_za_status["sdn_status"])
+
+        if len(sdn_status) == 1 and "ACTIVE" in sdn_status:
+            service_status = {"sdn_status": "ACTIVE"}
+        else:
+            service_status = {"sdn_status": "ERROR"}
+
+        return service_status
+
+    def create_connectivity_service(self, service_type, connection_points, **kwargs):
+        """
+        Establish SDN/WAN connectivity between the endpoints
+        :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``.
+        :param connection_points:  (list): each point corresponds to
+            an entry point to be connected. For WIM: from the DC to the transport network.
+            For SDN: Compute/PCI to the transport network. One
+            connection point serves to identify the specific access and
+            some other service parameters, such as encapsulation type.
+            Each item of the list is a dict with:
+                "service_endpoint_id": (str)(uuid)  Same meaning that for 'service_endpoint_mapping' (see __init__)
+                    In case the config attribute mapping_not_needed is True, this value is not relevant. In this case
+                    it will contain the string "device_id:device_interface_id"
+                "service_endpoint_encapsulation_type": None, "dot1q", ...
+                "service_endpoint_encapsulation_info": (dict) with:
+                    "vlan": ..., (int, present if encapsulation is dot1q)
+                    "vni": ... (int, present if encapsulation is vxlan),
+                    "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan)
+                    "mac": ...
+                    "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__)
+                    "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__)
+                    "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id
+                    "swith_port": ... present if mapping has been found for this device_id,device_interface_id
+                    "service_mapping_info": present if mapping has been found for this device_id,device_interface_id
+        :param kwargs: For future versions:
+            bandwidth (int): value in kilobytes
+            latency (int): value in milliseconds
+            Other QoS might be passed as keyword arguments.
+        :return: tuple: ``(service_id, conn_info)`` containing:
+            - *service_uuid* (str): UUID of the established connectivity service
+            - *conn_info* (dict or None): Information to be stored at the database (or ``None``).
+                This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`.
+                **MUST** be JSON/YAML-serializable (plain data structures).
+        :raises: SdnConnectorException: In case of error. Nothing should be created in this case.
+            Provide the parameter http_code
+        """
+        supported_service_types = {"ELINE"}
+        if service_type not in supported_service_types:
+            raise WimTapiUnsupportedServiceType(service_type, supported_service_types)
+
+        self.logger.debug("connection_points={:s}".format(str(connection_points)))
+
+        if not isinstance(connection_points, (list, tuple)):
+            raise WimTapiConnectionPointsBadFormat(connection_points)
+
+        if len(connection_points) != 2:
+            raise WimTapiConnectionPointsBadFormat(connection_points)
+
+        sips = self.tapi_client.get_service_interface_points()
+        services_composer = ServicesComposer(sips)
+
+        for connection_point in connection_points:
+            service_endpoint_id = connection_point.get("service_endpoint_id")
+            if service_endpoint_id is None:
+                raise WimTapiMissingConnPointField(
+                    connection_point, "service_endpoint_id"
+                )
+
+            mapping = self.mappings.get(service_endpoint_id, {})
+            services_composer.add_service_endpoint(service_endpoint_id, mapping)
+
+        services_composer.dump(self.logger)
+
+        service_uuid, conn_info = self._create_services_and_conn_info(services_composer)
+        return service_uuid, conn_info
+
+    def _create_services_and_conn_info(self, services_composer: ServicesComposer):
+        services = services_composer.services
+        requested_capacity = services_composer.requested_capacity
+        vlan_constraint = services_composer.vlan_constraint
+
+        service_uuid = str(uuid.uuid4())
+
+        if services_composer.is_bidirectional():
+            service_endpoints = services[0]
+            self.tapi_client.create_service(
+                "<>",
+                service_uuid,
+                service_endpoints,
+                bidirectional=True,
+                requested_capacity=requested_capacity,
+                vlan_constraint=vlan_constraint,
+            )
+            conn_info = conn_info_compose_bidirectional(
+                service_uuid,
+                service_endpoints,
+                requested_capacity=requested_capacity,
+                vlan_constraint=vlan_constraint,
+            )
+
+        else:
+            service_uuid = service_uuid[0 : len(service_uuid) - 4] + "00**"
+            service_az_uuid = service_uuid.replace("**", "af")
+            service_az_endpoints = services[0]
+            service_za_uuid = service_uuid.replace("**", "fa")
+            service_za_endpoints = services[1]
+
+            self.tapi_client.create_service(
+                ">>",
+                service_az_uuid,
+                service_az_endpoints,
+                bidirectional=False,
+                requested_capacity=requested_capacity,
+                vlan_constraint=vlan_constraint,
+            )
+            self.tapi_client.create_service(
+                "<<",
+                service_za_uuid,
+                service_za_endpoints,
+                bidirectional=False,
+                requested_capacity=requested_capacity,
+                vlan_constraint=vlan_constraint,
+            )
+            conn_info = conn_info_compose_unidirectional(
+                service_az_uuid,
+                service_az_endpoints,
+                service_za_uuid,
+                service_za_endpoints,
+                requested_capacity=requested_capacity,
+                vlan_constraint=vlan_constraint,
+            )
+
+        return service_uuid, conn_info
+
+    def delete_connectivity_service(self, service_uuid, conn_info=None):
+        """
+        Disconnect multi-site endpoints previously connected
+
+        :param service_uuid: The one returned by create_connectivity_service
+        :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
+            if they do not return None
+        :return: None
+        :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
+        """
+        bidirectional = conn_info["bidirectional"]
+        if bidirectional:
+            service_uuid = conn_info["uuid"]
+            self.tapi_client.delete_service("<>", service_uuid)
+        else:
+            service_az_uuid = conn_info["az"]["uuid"]
+            service_za_uuid = conn_info["za"]["uuid"]
+            self.tapi_client.delete_service(">>", service_az_uuid)
+            self.tapi_client.delete_service("<<", service_za_uuid)
+
+    def edit_connectivity_service(
+        self, service_uuid, conn_info=None, connection_points=None, **kwargs
+    ):
+        """Change an existing connectivity service.
+
+        This method's arguments and return value follow the same convention as
+        :meth:`~.create_connectivity_service`.
+
+        :param service_uuid: UUID of the connectivity service.
+        :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service
+            or edit_connectivity_service
+        :param connection_points: (list): If provided, the old list of connection points will be replaced.
+        :param kwargs: Same meaning that create_connectivity_service
+        :return: dict or None: Information to be updated and stored at the database.
+                When ``None`` is returned, no information should be changed.
+                When an empty dict is returned, the database record will be deleted.
+                **MUST** be JSON/YAML-serializable (plain data structures).
+        Raises:
+            SdnConnectorException: In case of error.
+        """
+        raise NotImplementedError
+
+    def clear_all_connectivity_services(self):
+        """Delete all WAN Links in a WIM.
+
+        This method is intended for debugging only, and should delete all the
+        connections controlled by the WIM/SDN, not only the  connections that
+        a specific RO is aware of.
+
+        Raises:
+            SdnConnectorException: In case of error.
+        """
+        raise NotImplementedError
+
+    def get_all_active_connectivity_services(self):
+        """Provide information about all active connections provisioned by a
+        WIM.
+
+        Raises:
+            SdnConnectorException: In case of error.
+        """
+        raise NotImplementedError
diff --git a/RO-SDN-tapi/requirements.in b/RO-SDN-tapi/requirements.in
new file mode 100644 (file)
index 0000000..25e572e
--- /dev/null
@@ -0,0 +1,17 @@
+# 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.
+##
+
+requests
diff --git a/RO-SDN-tapi/setup.py b/RO-SDN-tapi/setup.py
new file mode 100644 (file)
index 0000000..107778a
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+##
+# 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 setuptools import setup
+
+_name = "osm_rosdn_tapi"
+_version_command = ("git describe --match v* --tags --long --dirty", "pep440-git-full")
+_description = "OSM ro sdn plugin for tapi"
+_author = "OSM Support"
+_author_email = "osmsupport@etsi.org"
+_maintainer = "OSM Support"
+_maintainer_email = "osmsupport@etsi.org"
+_license = "Apache 2.0"
+_url = "https://osm.etsi.org/gitweb/?p=osm/RO.git;a=summary"
+
+_readme = """
+===========
+osm-rosdn_tapi
+===========
+
+osm-ro pluging for tapi SDN
+"""
+
+setup(
+    name=_name,
+    description=_description,
+    long_description=_readme,
+    version_command=_version_command,
+    author=_author,
+    author_email=_author_email,
+    maintainer=_maintainer,
+    maintainer_email=_maintainer_email,
+    url=_url,
+    license=_license,
+    packages=[_name],
+    include_package_data=True,
+    setup_requires=["setuptools-version-command"],
+    entry_points={
+        "osm_rosdn.plugins": [
+            "rosdn_tapi = osm_rosdn_tapi.wimconn_tapi:WimconnectorTAPI"
+        ],
+    },
+)
diff --git a/RO-SDN-tapi/stdeb.cfg b/RO-SDN-tapi/stdeb.cfg
new file mode 100644 (file)
index 0000000..262a47c
--- /dev/null
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+
+[DEFAULT]
+X-Python3-Version : >= 3.5
index b6fff18..2e1ddc6 100644 (file)
@@ -68,6 +68,7 @@ ip_addr1 = "20.3.4.5"
 volume_id = "ac408b73-b9cc-4a6a-a270-82cc4811bd4a"
 volume_id2 = "o4e0e83-b9uu-4akk-a234-89cc4811bd4a"
 volume_id3 = "44e0e83-t9uu-4akk-a234-p9cc4811bd4a"
+volume_id4 = "91bf5674-5b85-41d1-aa3b-4848e2691088"
 virtual_mac_id = "64e0e83-t9uu-4akk-a234-p9cc4811bd4a"
 created_items_all_true = {
     f"floating_ip:{floating_network_vim_id}": True,
@@ -108,12 +109,12 @@ def check_if_assert_not_called(mocks: list):
         mocking.assert_not_called()
 
 
-class Status:
-    def __init__(self, s):
+class Volume:
+    def __init__(self, s, type="__DEFAULT__", name="", id=""):
         self.status = s
-
-    def __str__(self):
-        return self.status
+        self.volume_type = type
+        self.name = name
+        self.id = id
 
 
 class CopyingMock(MagicMock):
@@ -1312,6 +1313,43 @@ class TestNewVmInstance(unittest.TestCase):
         self.assertEqual(existing_vim_volumes, expected_existing_vim_volumes)
         self.vimconn.cinder.volumes.create.assert_not_called()
 
+    @patch.object(vimconnector, "update_block_device_mapping")
+    def test__prepare_shared_volumes_vim_using_volume_id(
+        self, mock_update_block_device_mapping
+    ):
+        """Existing persistent non root volume with vim_volume_id.
+        class Volume:
+            def __init__(self, s, type="__DEFAULT__", name="", id=""):
+                self.status = s
+                self.volume_type = type
+                self.name = name
+                self.id = id
+        volumes = {"shared-volume": volume_id4}
+
+        The device mappeing BEFORE is: {}
+        The device mappeing AFTER is: {'vdb': '8ca50cc6-a779-4513-a1f3-900b8b3987d2'}
+        """
+        base_disk_index = ord("b")
+        disk = {"name": "shared-volume"}
+        block_device_mapping = {}
+        existing_vim_volumes = []
+        created_items = {}
+        expected_block_device_mapping = {}
+        self.vimconn.cinder.volumes.list.return_value = [
+            Volume("avaible", "multiattach", "shared-volume", volume_id4)
+        ]
+        self.vimconn.cinder.volumes.get.return_value.id = volume_id4
+        self.vimconn._prepare_shared_volumes(
+            name,
+            disk,
+            base_disk_index,
+            block_device_mapping,
+            existing_vim_volumes,
+            created_items,
+        )
+        self.vimconn.cinder.volumes.get.assert_called_with(volume_id4)
+        self.assertDictEqual(block_device_mapping, expected_block_device_mapping)
+
     @patch.object(vimconnector, "update_block_device_mapping")
     def test_prepare_persistent_non_root_volumes_vim_using_volume_id(
         self, mock_update_block_device_mapping
@@ -1577,6 +1615,22 @@ class TestNewVmInstance(unittest.TestCase):
             _call_mock_update_block_device_mapping[0].kwargs["created_items"], {}
         )
 
+    @patch.object(vimconnector, "update_block_device_mapping")
+    def test_new_shared_volumes(self, mock_update_block_device_mapping):
+        """Create shared volume."""
+        self.vimconn.cinder = CopyingMock()
+        self.vimconn.cinder.volumes.create.return_value.id = volume_id4
+        shared_volume_data = {"size": 10, "name": "shared-volume"}
+        self.vimconn.cinder.volumes.create.side_effect = [
+            Volume("avaible", "multiattach", "shared-volume", volume_id4)
+        ]
+        result = self.vimconn.new_shared_volumes(shared_volume_data)
+        self.vimconn.cinder.volumes.create.assert_called_once_with(
+            size=10, name="shared-volume", volume_type="multiattach"
+        )
+        self.assertEqual(result[0], "shared-volume")
+        self.assertEqual(result[1], volume_id4)
+
     @patch.object(vimconnector, "update_block_device_mapping")
     def test_prepare_persistent_root_volumes_create_raise_exception(
         self, mock_update_block_device_mapping
@@ -1689,9 +1743,9 @@ class TestNewVmInstance(unittest.TestCase):
             f"volume:{volume_id3}": True,
         }
         self.vimconn.cinder.volumes.get.side_effect = [
-            Status("processing"),
-            Status("available"),
-            Status("available"),
+            Volume("processing"),
+            Volume("available"),
+            Volume("available"),
         ]
 
         result = self.vimconn._wait_for_created_volumes_availability(
@@ -1716,9 +1770,9 @@ class TestNewVmInstance(unittest.TestCase):
             {"id": "44e0e83-b9uu-4akk-t234-p9cc4811bd4a"},
         ]
         self.vimconn.cinder.volumes.get.side_effect = [
-            Status("processing"),
-            Status("available"),
-            Status("available"),
+            Volume("processing"),
+            Volume("available", "multiattach"),
+            Volume("available"),
         ]
 
         result = self.vimconn._wait_for_existing_volumes_availability(
@@ -1742,8 +1796,8 @@ class TestNewVmInstance(unittest.TestCase):
         elapsed_time = 1805
         created_items = {f"volume:{volume_id2}": True}
         self.vimconn.cinder.volumes.get.side_effect = [
-            Status("processing"),
-            Status("processing"),
+            Volume("processing"),
+            Volume("processing"),
         ]
         with patch("time.sleep", mock_sleep):
             result = self.vimconn._wait_for_created_volumes_availability(
@@ -1761,8 +1815,8 @@ class TestNewVmInstance(unittest.TestCase):
         elapsed_time = 1805
         existing_vim_volumes = [{"id": volume_id2}]
         self.vimconn.cinder.volumes.get.side_effect = [
-            Status("processing"),
-            Status("processing"),
+            Volume("processing"),
+            Volume("processing"),
         ]
 
         result = self.vimconn._wait_for_existing_volumes_availability(
@@ -3950,6 +4004,15 @@ class TestNewVmInstance(unittest.TestCase):
         self.vimconn.logger.error.assert_not_called()
         self.assertEqual(created_items, expected_created_items)
 
+    def test_delete_shared_volumes(self):
+        """cinder delete shared volumes"""
+        shared_volume_vim_id = volume_id4
+        self.vimconn.cinder.volumes.get.return_value.status = "available"
+        self.vimconn.delete_shared_volumes(shared_volume_vim_id)
+        self.vimconn.cinder.volumes.get.assert_called_once_with(shared_volume_vim_id)
+        self.vimconn.cinder.volumes.delete.assert_called_once_with(shared_volume_vim_id)
+        self.vimconn.logger.error.assert_not_called()
+
     def test_delete_volumes_by_id_with_cinder_get_volume_raise_exception(self):
         """cinder get volume raises exception."""
         created_items = {
index f1df8a7..82a1e37 100644 (file)
@@ -339,7 +339,7 @@ class vimconnector(vimconn.VimConnector):
             version = self.config.get("microversion")
 
             if not version:
-                version = "2.1"
+                version = "2.60"
 
             # addedd region_name to keystone, nova, neutron and cinder to support distributed cloud for Wind River
             # Titanium cloud and StarlingX
@@ -2151,6 +2151,38 @@ class vimconnector(vimconn.VimConnector):
         created_items[volume_txt] = True
         block_device_mapping["vd" + chr(base_disk_index)] = volume.id
 
+    def new_shared_volumes(self, shared_volume_data) -> (str, str):
+        try:
+            volume = self.cinder.volumes.create(
+                size=shared_volume_data["size"],
+                name=shared_volume_data["name"],
+                volume_type="multiattach",
+            )
+            return (volume.name, volume.id)
+        except (ConnectionError, KeyError) as e:
+            self._format_exception(e)
+
+    def _prepare_shared_volumes(
+        self,
+        name: str,
+        disk: dict,
+        base_disk_index: int,
+        block_device_mapping: dict,
+        existing_vim_volumes: list,
+        created_items: dict,
+    ):
+        volumes = {volume.name: volume.id for volume in self.cinder.volumes.list()}
+        if volumes.get(disk["name"]):
+            sv_id = volumes[disk["name"]]
+            volume = self.cinder.volumes.get(sv_id)
+            self.update_block_device_mapping(
+                volume=volume,
+                block_device_mapping=block_device_mapping,
+                base_disk_index=base_disk_index,
+                disk=disk,
+                created_items=created_items,
+            )
+
     def _prepare_non_root_persistent_volumes(
         self,
         name: str,
@@ -2175,17 +2207,15 @@ class vimconnector(vimconn.VimConnector):
         # Non-root persistent volumes
         # Disk may include only vim_volume_id or only vim_id."
         key_id = "vim_volume_id" if "vim_volume_id" in disk.keys() else "vim_id"
-
         if disk.get(key_id):
             # Use existing persistent volume
             block_device_mapping["vd" + chr(base_disk_index)] = disk[key_id]
             existing_vim_volumes.append({"id": disk[key_id]})
-
         else:
-            # Create persistent volume
+            volume_name = f"{name}vd{chr(base_disk_index)}"
             volume = self.cinder.volumes.create(
                 size=disk["size"],
-                name=name + "vd" + chr(base_disk_index),
+                name=volume_name,
                 # Make sure volume is in the same AZ as the VM to be attached to
                 availability_zone=vm_av_zone,
             )
@@ -2210,7 +2240,6 @@ class vimconnector(vimconn.VimConnector):
             elapsed_time    (int):          Time spent while waiting
 
         """
-
         while elapsed_time < volume_timeout:
             for created_item in created_items:
                 v, volume_id = (
@@ -2218,7 +2247,13 @@ class vimconnector(vimconn.VimConnector):
                     created_item.split(":")[1],
                 )
                 if v == "volume":
-                    if self.cinder.volumes.get(volume_id).status != "available":
+                    volume = self.cinder.volumes.get(volume_id)
+                    if (
+                        volume.volume_type == "multiattach"
+                        and volume.status == "in-use"
+                    ):
+                        return elapsed_time
+                    elif volume.status != "available":
                         break
             else:
                 # All ready: break from while
@@ -2245,7 +2280,10 @@ class vimconnector(vimconn.VimConnector):
 
         while elapsed_time < volume_timeout:
             for volume in existing_vim_volumes:
-                if self.cinder.volumes.get(volume["id"]).status != "available":
+                v = self.cinder.volumes.get(volume["id"])
+                if v.volume_type == "multiattach" and v.status == "in-use":
+                    return elapsed_time
+                elif v.status != "available":
                     break
             else:  # all ready: break from while
                 break
@@ -2279,7 +2317,6 @@ class vimconnector(vimconn.VimConnector):
         base_disk_index = ord("b")
         boot_volume_id = None
         elapsed_time = 0
-
         for disk in disk_list:
             if "image_id" in disk:
                 # Root persistent volume
@@ -2293,6 +2330,15 @@ class vimconnector(vimconn.VimConnector):
                     existing_vim_volumes=existing_vim_volumes,
                     created_items=created_items,
                 )
+            elif disk.get("multiattach"):
+                self._prepare_shared_volumes(
+                    name=name,
+                    disk=disk,
+                    base_disk_index=base_disk_index,
+                    block_device_mapping=block_device_mapping,
+                    existing_vim_volumes=existing_vim_volumes,
+                    created_items=created_items,
+                )
             else:
                 # Non-root persistent volume
                 self._prepare_non_root_persistent_volumes(
@@ -2748,7 +2794,6 @@ class vimconnector(vimconn.VimConnector):
                     server_group_id,
                 )
             )
-
             # Create VM
             server = self.nova.servers.create(
                 name=name,
@@ -2918,6 +2963,23 @@ class vimconnector(vimconn.VimConnector):
         except Exception as e:
             self.logger.error("Error deleting port: {}: {}".format(type(e).__name__, e))
 
+    def delete_shared_volumes(self, shared_volume_vim_id: str) -> bool:
+        """Cinder delete volume by id.
+        Args:
+            shared_volume_vim_id    (str):                  ID of shared volume in VIM
+        """
+        try:
+            if self.cinder.volumes.get(shared_volume_vim_id).status != "available":
+                return True
+
+            else:
+                self.cinder.volumes.delete(shared_volume_vim_id)
+
+        except Exception as e:
+            self.logger.error(
+                "Error deleting volume: {}: {}".format(type(e).__name__, e)
+            )
+
     def _delete_volumes_by_id_wth_cinder(
         self, k: str, k_id: str, volumes_to_hold: list, created_items: dict
     ) -> bool:
@@ -2995,7 +3057,6 @@ class vimconnector(vimconn.VimConnector):
 
             try:
                 k_item, k_id = self._get_item_name_id(k)
-
                 if k_item == "volume":
                     unavailable_vol = self._delete_volumes_by_id_wth_cinder(
                         k, k_id, volumes_to_hold, created_items
index 3ac7ab6..3d570c7 100755 (executable)
@@ -29,6 +29,7 @@ dist_ro_sdn_juniper_contrail
 dist_ro_sdn_odl_of
 dist_ro_sdn_onos_of
 dist_ro_sdn_onos_vpls
+dist_ro_sdn_tapi
 dist_ro_vim_aws
 dist_ro_vim_azure
 dist_ro_vim_openstack
@@ -53,7 +54,7 @@ do
     cp ${vim_plugin}/deb_dist/python3-osm-rovim*.deb deb_dist/
 done
 
-# SDN plugins: DynPac, Ietfl2vpn, Onosof Floodlightof
+# SDN plugins: DynPac, Ietfl2vpn, Onosof Floodlightof, Transport API (TAPI)
 for sdn_plugin in RO-SDN-*
 do
     cp ${sdn_plugin}/deb_dist/python3-osm-rosdn*.deb deb_dist/
diff --git a/releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml b/releasenotes/notes/feature_10937_Transport_API_WIM_connector_for_RO-9dc1e3aba7e1f3ad.yaml
new file mode 100644 (file)
index 0000000..70c79ea
--- /dev/null
@@ -0,0 +1,20 @@
+#######################################################################################
+# 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.
+#######################################################################################
+---
+features:
+  - |
+    Feature 10937: Transport API (TAPI) WIM connector for RO
diff --git a/releasenotes/notes/feature_10972_Support_of_volume_multi_attach-57c1232c1a54ab18.yaml b/releasenotes/notes/feature_10972_Support_of_volume_multi_attach-57c1232c1a54ab18.yaml
new file mode 100644 (file)
index 0000000..040aef3
--- /dev/null
@@ -0,0 +1,21 @@
+#######################################################################################
+# 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.
+#######################################################################################
+---
+features:
+  - |
+    Feature 10972: Support of volume multi-attach
+    Support of volume multi-attach for Openstack based VIMs (server groups)
\ No newline at end of file
index 25cd571..64a1b2c 100644 (file)
@@ -20,7 +20,7 @@ async-timeout==4.0.2
     # via
     #   -r https://osm.etsi.org/gitweb/?p=osm/common.git;a=blob_plain;f=requirements.txt;hb=paas
     #   aiokafka
-charset-normalizer==3.0.1
+charset-normalizer==3.1.0
     # via -r https://osm.etsi.org/gitweb/?p=osm/common.git;a=blob_plain;f=requirements.txt;hb=paas
 dataclasses==0.6
     # via -r https://osm.etsi.org/gitweb/?p=osm/common.git;a=blob_plain;f=requirements.txt;hb=paas
index d7e02f1..f4344b4 100644 (file)
@@ -25,6 +25,7 @@
 -r RO-SDN-odl_openflow/requirements.in
 -r RO-SDN-onos_openflow/requirements.in
 -r RO-SDN-onos_vpls/requirements.in
+-r RO-SDN-tapi/requirements.in
 -r RO-VIM-aws/requirements.in
 -r RO-VIM-azure/requirements.in
 -r RO-VIM-openstack/requirements.in
diff --git a/tox.ini b/tox.ini
index c971bc1..4422084 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -57,6 +57,7 @@ commands =
         black --check --diff RO-SDN-odl_openflow
         black --check --diff RO-SDN-onos_openflow
         black --check --diff RO-SDN-onos_vpls
+        black --check --diff RO-SDN-tapi
         black --check --diff RO-VIM-aws
         black --check --diff RO-VIM-azure
         black --check --diff RO-VIM-openstack
@@ -109,6 +110,9 @@ commands =
         # RO-SDN-onos_vpls
         # nose2 -C --coverage RO-SDN-onos_vpls/osm_rosdn_onos_vpls -s RO-SDN-onos_vpls/osm_rosdn_onos_vpls
         # sh -c 'mv .coverage .coverage_rosdn_onos_vpls'
+        # RO-SDN-tapi
+        nose2 -C --coverage RO-SDN-tapi/osm_rosdn_tapi -s RO-SDN-tapi/osm_rosdn_tapi
+        sh -c 'mv .coverage .coverage_rosdn_tapi'
         # RO-VIM-aws
         # nose2 -C --coverage RO-VIM-aws/osm_rovim_aws -s RO-VIM-aws/osm_rovim_aws
         # sh -c 'mv .coverage .coverage_rovim_aws'
@@ -128,7 +132,7 @@ commands =
         # nose2 -C --coverage RO-VIM-gcp/osm_rovim_gcp -s RO-VIM-gcp/osm_rovim_gcp
         # sh -c 'mv .coverage .coverage_rovim_gcp'
         # Combine results and generate reports
-        # coverage combine .coverage_ng_ro .coverage_ro_plugin .coverage_rosdn_arista_cloudvision .coverage_rosdn_dpb .coverage_rosdn_dynpac .coverage_rosdn_floodlightof .coverage_rosdn_ietfl2vpn .coverage_rosdn_juniper_contrail .coverage_rosdn_odlof .coverage_rosdn_onos_vpls .coverage_rosdn_onosof .coverage_rovim_aws .coverage_rovim_azure .coverage_rovim_openvim .coverage_rovim_gcp # .coverage_rovim_openstack .coverage_rovim_vmware
+        # coverage combine .coverage_ng_ro .coverage_ro_plugin .coverage_rosdn_arista_cloudvision .coverage_rosdn_dpb .coverage_rosdn_dynpac .coverage_rosdn_floodlightof .coverage_rosdn_ietfl2vpn .coverage_rosdn_juniper_contrail .coverage_rosdn_odlof .coverage_rosdn_onos_vpls .coverage_rosdn_onosof .coverage_rosdn_tapi .coverage_rovim_aws .coverage_rovim_azure .coverage_rovim_openvim .coverage_rovim_gcp # .coverage_rovim_openstack .coverage_rovim_vmware
         coverage combine .coverage_ng_ro .coverage_rovim_openstack .coverage_rosdn_juniper_contrail
         coverage report --omit='*tests*'
         coverage html -d ./cover --omit='*tests*'
@@ -153,6 +157,7 @@ commands =
         flake8 RO-SDN-odl_openflow/osm_rosdn_odlof/ RO-SDN-odl_openflow/setup.py
         flake8 RO-SDN-onos_openflow/osm_rosdn_onosof/ RO-SDN-onos_openflow/setup.py
         flake8 RO-SDN-onos_vpls/osm_rosdn_onos_vpls/ RO-SDN-onos_vpls/setup.py
+        flake8 RO-SDN-tapi/osm_rosdn_tapi/ RO-SDN-tapi/setup.py
         flake8 RO-VIM-aws/osm_rovim_aws/ RO-VIM-aws/setup.py
         flake8 RO-VIM-azure/osm_rovim_azure/ RO-VIM-azure/setup.py
         flake8 RO-VIM-openstack/osm_rovim_openstack/ RO-VIM-openstack/setup.py
@@ -181,6 +186,7 @@ commands =
         pylint -E RO-SDN-odl_openflow/osm_rosdn_odlof
         pylint -E RO-SDN-onos_openflow/osm_rosdn_onosof
         pylint -E RO-SDN-onos_vpls/osm_rosdn_onos_vpls --disable=E1101
+        pylint -E RO-SDN-tapi/osm_rosdn_tapi
         pylint -E RO-VIM-aws/osm_rovim_aws
         pylint -E RO-VIM-azure/osm_rovim_azure --disable=all
         pylint -E RO-VIM-openstack/osm_rovim_openstack --disable=E1101
@@ -362,6 +368,19 @@ commands =
         sh -c 'cd deb_dist/osm-rosdn-onos-vpls*/ && dpkg-buildpackage -rfakeroot -uc -us'
 
 
+#######################################################################################
+[testenv:dist_ro_sdn_tapi]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-dist.txt
+skip_install = true
+whitelist_externals = sh
+changedir = {toxinidir}/RO-SDN-tapi
+commands =
+        sh -c 'rm -rf deb_dist dist osm_rosdn_tapi.egg-info osm_rosdn_tapi*.tar.gz'
+        python3 setup.py --command-packages=stdeb.command sdist_dsc
+        sh -c 'cd deb_dist/osm-rosdn-tapi*/ && dpkg-buildpackage -rfakeroot -uc -us'
+
+
 #######################################################################################
 [testenv:dist_ro_vim_aws]
 deps =  {[testenv]deps}