Feature 10972: Support of volume multi-attach 64/13064/23
authorvegall <lvega@whitestack.com>
Fri, 17 Mar 2023 15:09:50 +0000 (15:09 +0000)
committeraticig <gulsum.atici@canonical.com>
Mon, 29 May 2023 13:54:07 +0000 (15:54 +0200)
Change-Id: I6e88ee52e5e882dbb4ec7d66cf648fbe07d40509
Signed-off-by: vegall <lvega@whitestack.com>
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-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py
RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py
releasenotes/notes/feature_10972_Support_of_volume_multi_attach-57c1232c1a54ab18.yaml [new file with mode: 0644]

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": {
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
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