From: vegall Date: Fri, 17 Mar 2023 15:09:50 +0000 (+0000) Subject: Feature 10972: Support of volume multi-attach X-Git-Tag: release-v14.0-start~6 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=364627c364a86a85696781766326dd690a362bc4;p=osm%2FRO.git Feature 10972: Support of volume multi-attach Change-Id: I6e88ee52e5e882dbb4ec7d66cf648fbe07d40509 Signed-off-by: vegall --- diff --git a/NG-RO/osm_ng_ro/ns.py b/NG-RO/osm_ng_ro/ns.py index fd0ad07a..75bae1b1 100644 --- a/NG-RO/osm_ng_ro/ns.py +++ b/NG-RO/osm_ng_ro/ns.py @@ -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, diff --git a/NG-RO/osm_ng_ro/ns_thread.py b/NG-RO/osm_ng_ro/ns_thread.py index 03e8b30e..03255e3f 100644 --- a/NG-RO/osm_ng_ro/ns_thread.py +++ b/NG-RO/osm_ng_ro/ns_thread.py @@ -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, diff --git a/NG-RO/osm_ng_ro/tests/test_ns.py b/NG-RO/osm_ng_ro/tests/test_ns.py index d966a861..a2368195 100644 --- a/NG-RO/osm_ng_ro/tests/test_ns.py +++ b/NG-RO/osm_ng_ro/tests/test_ns.py @@ -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( diff --git a/NG-RO/osm_ng_ro/tests/test_ns_thread.py b/NG-RO/osm_ng_ro/tests/test_ns_thread.py index 0914a066..4e42b4f3 100644 --- a/NG-RO/osm_ng_ro/tests/test_ns_thread.py +++ b/NG-RO/osm_ng_ro/tests/test_ns_thread.py @@ -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" diff --git a/NG-RO/osm_ng_ro/validation.py b/NG-RO/osm_ng_ro/validation.py index 2601e90c..e4eed742 100644 --- a/NG-RO/osm_ng_ro/validation.py +++ b/NG-RO/osm_ng_ro/validation.py @@ -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-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py b/RO-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py index b6fff180..2e1ddc6a 100644 --- a/RO-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py +++ b/RO-VIM-openstack/osm_rovim_openstack/tests/test_vimconn_openstack.py @@ -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 = { diff --git a/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py b/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py index f1df8a73..82a1e37a 100644 --- a/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py +++ b/RO-VIM-openstack/osm_rovim_openstack/vimconn_openstack.py @@ -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 index 00000000..040aef3c --- /dev/null +++ b/releasenotes/notes/feature_10972_Support_of_volume_multi_attach-57c1232c1a54ab18.yaml @@ -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