From a1898b41202f597c147a16ad5db0992089c7d098 Mon Sep 17 00:00:00 2001 From: Gulsum Atici Date: Tue, 25 Apr 2023 15:48:10 +0300 Subject: [PATCH] OSMENG-1047 Use constraints from VDU definition Change-Id: Ib69783e31ec71f38cc9871796fcfe2f19f179268 Signed-off-by: Gulsum Atici Signed-off-by: Mark Beierl --- osm_lcm/nglcm.py | 1 + osm_lcm/temporal/juju_paas_activities.py | 21 +- osm_lcm/temporal/lcm_workflows.py | 4 +- osm_lcm/temporal/vnf_activities.py | 33 ++ osm_lcm/temporal/vnf_workflows.py | 70 +++- osm_lcm/tests/test_juju_paas_activities.py | 114 +++++- osm_lcm/tests/test_vnf_activities.py | 109 +++++- osm_lcm/tests/test_vnf_workflows.py | 383 +++++++++++++++++++-- 8 files changed, 680 insertions(+), 55 deletions(-) diff --git a/osm_lcm/nglcm.py b/osm_lcm/nglcm.py index 3aeeee2..729b775 100644 --- a/osm_lcm/nglcm.py +++ b/osm_lcm/nglcm.py @@ -174,6 +174,7 @@ class NGLcm: vnf_data_activity_instance.change_vnf_state, vnf_data_activity_instance.change_vnf_instantiation_state, vnf_operation_instance.get_task_queue, + vnf_operation_instance.get_vim_cloud, vnf_operation_instance.get_vnf_details, vnf_send_notifications_instance.send_notification_for_vnf, vnf_data_activity_instance.set_vnf_model, diff --git a/osm_lcm/temporal/juju_paas_activities.py b/osm_lcm/temporal/juju_paas_activities.py index d86f1c2..3182317 100644 --- a/osm_lcm/temporal/juju_paas_activities.py +++ b/osm_lcm/temporal/juju_paas_activities.py @@ -25,6 +25,7 @@ from osm_common.dataclasses.temporal_dataclasses import ( CheckCharmStatusInput, ModelInfo, TestVimConnectivityInput, + VduComputeConstraints, VduInstantiateInput, ) from osm_common.temporal_constants import ( @@ -107,6 +108,20 @@ class JujuPaasConnector: ) return controller + @staticmethod + def _get_application_constraints( + constraints: VduComputeConstraints, cloud: str + ) -> dict: + application_constraints = {} + if constraints.mem: + # Converting memory to MB as this is provided in GB + application_constraints["mem"] = constraints.mem * 1024 + # Kubernetes cloud does not support setting cores. + # https://juju.is/docs/olm/constraint#heading--kubernetes + if constraints.cores and cloud not in ["microk8s", "kubernetes"]: + application_constraints["cores"] = constraints.cores + return application_constraints + @activity.defn(name=ACTIVITY_TEST_VIM_CONNECTIVITY) async def test_vim_connectivity( self, test_connectivity_input: TestVimConnectivityInput @@ -134,7 +149,7 @@ class JujuPaasConnector: and wait on each connection attempt. """ vim_id = test_connectivity_input.vim_uuid - self._get_controller(vim_id) + await self._get_controller(vim_id) message = f"Connection to juju controller succeeded for {vim_id}" self.logger.info(message) @@ -218,6 +233,9 @@ class JujuPaasConnector: model_name = deploy_charm_input.model_name charm_info = deploy_charm_input.charm_info application_name = charm_info.app_name + constraints = JujuPaasConnector._get_application_constraints( + deploy_charm_input.constraints, deploy_charm_input.cloud + ) controller = await self._get_controller(deploy_charm_input.vim_uuid) model = await controller.get_model(model_name) if application_name in model.applications: @@ -226,6 +244,7 @@ class JujuPaasConnector: entity_url=charm_info.entity_url, application_name=application_name, channel=charm_info.channel, + constraints=constraints if constraints else None, ) @activity.defn(name=ACTIVITY_CHECK_CHARM_STATUS) diff --git a/osm_lcm/temporal/lcm_workflows.py b/osm_lcm/temporal/lcm_workflows.py index 9ee94d8..2dc7096 100644 --- a/osm_lcm/temporal/lcm_workflows.py +++ b/osm_lcm/temporal/lcm_workflows.py @@ -15,7 +15,6 @@ # limitations under the License. import logging -import traceback from abc import ABC, abstractmethod from datetime import timedelta from temporalio import workflow @@ -95,8 +94,7 @@ class LcmOperationWorkflow(ABC): raise e except Exception as e: - err_details = str(traceback.format_exc()) - self.logger.error(err_details) + self.logger.exception(e) await self.update_operation_state( LcmOperationState.FAILED, error_message=str(e), diff --git a/osm_lcm/temporal/vnf_activities.py b/osm_lcm/temporal/vnf_activities.py index 85de719..6f64a82 100644 --- a/osm_lcm/temporal/vnf_activities.py +++ b/osm_lcm/temporal/vnf_activities.py @@ -20,6 +20,7 @@ from osm_common.temporal_constants import ( ACTIVITY_CHANGE_VNF_STATE, ACTIVITY_CHANGE_VNF_INSTANTIATION_STATE, ACTIVITY_GET_TASK_QUEUE, + ACTIVITY_GET_VIM_CLOUD, ACTIVITY_GET_VNF_DETAILS, ACTIVITY_SEND_NOTIFICATION_FOR_VNF, ACTIVITY_SET_VNF_MODEL, @@ -30,6 +31,8 @@ from osm_common.dataclasses.temporal_dataclasses import ( ChangeVnfStateInput, GetTaskQueueInput, GetTaskQueueOutput, + GetVimCloudInput, + GetVimCloudOutput, GetVnfDetailsInput, GetVnfDetailsOutput, VnfInstantiateInput, @@ -72,6 +75,36 @@ class VnfOperations: self.logger.debug(f"Got the task queue {task_queue} for VNF operations.") return GetTaskQueueOutput(task_queue) + @activity.defn(name=ACTIVITY_GET_VIM_CLOUD) + async def get_vim_cloud( + self, get_vim_cloud_input: GetVimCloudInput + ) -> GetVimCloudOutput: + """Finds the cloud by checking the VIM account of VNF. + + Collaborators: + DB Access Object + + Raises (retryable): + DbException: If DB read operations fail, the collection or DB record ID does not exist. + + Activity Lifecycle: + This activity should complete relatively quickly (less than a + second). However, it would be reasonable to wait up to 10 + seconds. + + This activity will not report a heartbeat due to its + short-running nature. + + It is not necessary to implement a back-off strategy for this + activity, the operation is idempotent. + + """ + vnfr = self.db.get_one("vnfrs", {"_id": get_vim_cloud_input.vnfr_uuid}) + vim_record = self.db.get_one("vim_accounts", {"_id": vnfr["vim-account-id"]}) + cloud = vim_record["config"].get("cloud", "") + self.logger.debug(f"Got the cloud type {cloud} for VNF operations.") + return GetVimCloudOutput(cloud=cloud) + @activity.defn(name=ACTIVITY_GET_VNF_DETAILS) async def get_vnf_details( self, get_vnf_details_input: GetVnfDetailsInput diff --git a/osm_lcm/temporal/vnf_workflows.py b/osm_lcm/temporal/vnf_workflows.py index ffdd403..6c7eae0 100644 --- a/osm_lcm/temporal/vnf_workflows.py +++ b/osm_lcm/temporal/vnf_workflows.py @@ -27,6 +27,8 @@ from osm_common.dataclasses.temporal_dataclasses import ( ChangeVnfStateInput, GetTaskQueueInput, GetTaskQueueOutput, + GetVimCloudInput, + GetVimCloudOutput, GetVnfDetailsInput, GetVnfDetailsOutput, VduComputeConstraints, @@ -41,6 +43,7 @@ from osm_common.temporal_constants import ( ACTIVITY_SEND_NOTIFICATION_FOR_VNF, ACTIVITY_CHANGE_VNF_STATE, ACTIVITY_GET_TASK_QUEUE, + ACTIVITY_GET_VIM_CLOUD, ACTIVITY_GET_VNF_DETAILS, ACTIVITY_SET_VNF_MODEL, LCM_TASK_QUEUE, @@ -114,8 +117,23 @@ class VnfInstantiateWorkflow: ), ) + get_cloud = value_to_type( + GetVimCloudOutput, + await workflow.execute_activity( + activity=ACTIVITY_GET_VIM_CLOUD, + arg=GetVimCloudInput(input.vnfr_uuid), + activity_id=f"{ACTIVITY_GET_VIM_CLOUD}-{input.vnfr_uuid}", + task_queue=vnf_task_queue.task_queue, + schedule_to_close_timeout=default_schedule_to_close_timeout, + retry_policy=retry_policy, + ), + ) + await self.instantiate_vdus( - get_vnf_details.vnfr, get_vnf_details.vnfd, vnf_task_queue.task_queue + vnfr=get_vnf_details.vnfr, + vnfd=get_vnf_details.vnfd, + task_queue=vnf_task_queue.task_queue, + cloud=get_cloud.cloud, ) await self.update_states( vnf_instantiation_state=ChangeVnfInstantiationStateInput( @@ -189,12 +207,14 @@ class VnfInstantiateWorkflow: ) @staticmethod - async def instantiate_vdus(vnfr: dict, vnfd: dict, task_queue: str): + async def instantiate_vdus(vnfr: dict, vnfd: dict, task_queue: str, cloud: str): for vdu in vnfd.get("vdu"): ( vdu_instantiate_input, vdu_instantiate_workflow_id, - ) = VnfInstantiateWorkflow.get_vdu_instantiate_input(vnfr, vnfd, vdu) + ) = VnfInstantiateWorkflow.get_vdu_instantiate_input( + vnfr=vnfr, vnfd=vnfd, vdu=vdu, cloud=cloud + ) await workflow.execute_child_workflow( workflow=WORKFLOW_VDU_INSTANTIATE, arg=vdu_instantiate_input, @@ -203,14 +223,52 @@ class VnfInstantiateWorkflow: ) @staticmethod - def get_vdu_instantiate_input(vnfr: dict, vnfd: dict, vdu: dict): + def get_flavor_details(compute_desc_id: str, vnfd: dict): + if not compute_desc_id: + return {} + flavor_details = next( + filter( + lambda flavor: flavor.get("id") == compute_desc_id, + vnfd.get("virtual-compute-desc", {}), + ), + {}, + ) + return flavor_details + + @staticmethod + def get_compute_constraints(vdu: dict, vnfd: dict) -> VduComputeConstraints: + compute_desc_id = vdu.get("virtual-compute-desc", "") + flavor_details = VnfInstantiateWorkflow.get_flavor_details( + compute_desc_id, vnfd + ) + if not flavor_details: + return VduComputeConstraints(cores=0, mem=0) + + cpu_cores = ( + flavor_details["virtual-cpu"].get("num-virtual-cpu", 0) + if flavor_details.get("virtual-cpu") + else 0 + ) + memory_gb = ( + flavor_details["virtual-memory"].get("size", 0) + if flavor_details.get("virtual-memory") + else 0 + ) + return VduComputeConstraints(cores=cpu_cores, mem=int(memory_gb)) + + @staticmethod + def get_vdu_instantiate_input(vnfr: dict, vnfd: dict, vdu: dict, cloud: str): model_name = vnfr.get("namespace") vim_id = vnfr.get("vim-account-id") sw_image_descs = vnfd.get("sw-image-desc") vdu_info = CharmInfoUtils.get_charm_info(vdu, sw_image_descs) - compute_constraints = VduComputeConstraints(cores=0, mem=0) + compute_constraints = VnfInstantiateWorkflow.get_compute_constraints(vdu, vnfd) vdu_instantiate_input = VduInstantiateInput( - vim_id, model_name, vdu_info, compute_constraints, "cloud" + vim_uuid=vim_id, + model_name=model_name, + charm_info=vdu_info, + constraints=compute_constraints, + cloud=cloud, ) vdu_instantiate_workflow_id = ( vdu_instantiate_input.model_name diff --git a/osm_lcm/tests/test_juju_paas_activities.py b/osm_lcm/tests/test_juju_paas_activities.py index d104d07..cb06f26 100644 --- a/osm_lcm/tests/test_juju_paas_activities.py +++ b/osm_lcm/tests/test_juju_paas_activities.py @@ -16,6 +16,7 @@ import asynctest import asyncio +from unittest import TestCase import unittest.mock as mock from juju.application import Application @@ -256,24 +257,89 @@ class TestDeployCharm(TestJujuPaasActivitiesBase): app_name = "my_app_name" channel = "latest" entity_url = "ch:my-charm" + cloud_k8s = "microk8s" + cloud_other = "other" charm_info = CharmInfo(app_name, channel, entity_url) - constraints = VduComputeConstraints(1, 2) - vdu_instantiate_input = VduInstantiateInput( - vim_content["_id"], namespace, charm_info, constraints, cloud="" + vdu_instantiate_input_with_constraints_k8s = VduInstantiateInput( + vim_content["_id"], + namespace, + charm_info, + VduComputeConstraints(mem=1, cores=1), + cloud_k8s, ) - async def test_deploy_charm_nominal_case(self): - await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input) + async def test_deploy_charm_with_constraints_k8s_cloud(self): + await self.env.run( + self.juju_paas.deploy_charm, self.vdu_instantiate_input_with_constraints_k8s + ) + self.model.deploy.assert_called_once_with( + entity_url=self.entity_url, + application_name=self.app_name, + channel=self.channel, + constraints={"mem": 1024}, + ) + + async def test_deploy_charm_with_constraints_other_cloud(self): + await self.env.run( + self.juju_paas.deploy_charm, + VduInstantiateInput( + vim_content["_id"], + namespace, + self.charm_info, + VduComputeConstraints(mem=1, cores=1), + self.cloud_other, + ), + ) self.model.deploy.assert_called_once_with( entity_url=self.entity_url, application_name=self.app_name, channel=self.channel, + constraints={"mem": 1024, "cores": 1}, + ) + + async def test_deploy_charm_without_constraints_k8s_cloud(self): + await self.env.run( + self.juju_paas.deploy_charm, + VduInstantiateInput( + vim_content["_id"], + namespace, + self.charm_info, + VduComputeConstraints(mem=0, cores=0), + self.cloud_k8s, + ), + ) + self.model.deploy.assert_called_once_with( + entity_url=self.entity_url, + application_name=self.app_name, + channel=self.channel, + constraints=None, + ) + + async def test_deploy_charm_without_constraints_other_cloud(self): + await self.env.run( + self.juju_paas.deploy_charm, + VduInstantiateInput( + vim_content["_id"], + namespace, + self.charm_info, + VduComputeConstraints(mem=0, cores=0), + self.cloud_other, + ), + ) + self.model.deploy.assert_called_once_with( + entity_url=self.entity_url, + application_name=self.app_name, + channel=self.channel, + constraints=None, ) async def test_deploy_charm_app_already_exists(self): self.add_application(self.app_name) with self.assertRaises(Exception) as err: - await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input) + await self.env.run( + self.juju_paas.deploy_charm, + self.vdu_instantiate_input_with_constraints_k8s, + ) self.model.deploy.assert_not_called() self.assertEqual( str(err.exception.args[0]), @@ -283,10 +349,44 @@ class TestDeployCharm(TestJujuPaasActivitiesBase): async def test_deploy_charm_raises_exception(self): self.controller.get_model.side_effect = JujuError() with self.assertRaises(JujuError): - await self.env.run(self.juju_paas.deploy_charm, self.vdu_instantiate_input) + await self.env.run( + self.juju_paas.deploy_charm, + self.vdu_instantiate_input_with_constraints_k8s, + ) self.model.deploy.assert_not_called() +class TestGetApplicationConstraints(TestCase): + constraints = VduComputeConstraints(mem=1, cores=1) + no_constraints = VduComputeConstraints(mem=0, cores=0) + + def test_get_application_constraints_k8s_cloud(self): + result = JujuPaasConnector._get_application_constraints( + self.constraints, "kubernetes" + ) + self.assertEqual(result, {"mem": 1024}) + + def test_get_application_constraints_aws_cloud(self): + result = JujuPaasConnector._get_application_constraints(self.constraints, "aws") + self.assertEqual(result, {"cores": 1, "mem": 1024}) + + def test_get_application_constraints_no_constraints_aws(self): + result = JujuPaasConnector._get_application_constraints( + self.no_constraints, "aws" + ) + self.assertEqual(result, {}) + + def test_get_application_constraints_no_constraints_microk8s(self): + result = JujuPaasConnector._get_application_constraints( + self.no_constraints, "microk8s" + ) + self.assertEqual(result, {}) + + def test_get_application_constraints_empty_cloud(self): + result = JujuPaasConnector._get_application_constraints(self.constraints, "") + self.assertEqual(result, {"cores": 1, "mem": 1024}) + + class TestTestVimConnectivity(TestJujuPaasActivitiesBase): test_vim_connectivity_input = TestVimConnectivityInput(vim_content["_id"]) diff --git a/osm_lcm/tests/test_vnf_activities.py b/osm_lcm/tests/test_vnf_activities.py index 12905fd..592c523 100644 --- a/osm_lcm/tests/test_vnf_activities.py +++ b/osm_lcm/tests/test_vnf_activities.py @@ -19,6 +19,7 @@ import asynctest from asyncio.exceptions import CancelledError from copy import deepcopy from osm_common.dataclasses.temporal_dataclasses import ( + GetVimCloudInput, GetVnfDetailsInput, VnfInstantiateInput, ) @@ -30,6 +31,37 @@ from unittest.mock import Mock vnfr_uuid = "d08d2da5-2120-476c-8538-deaeb4e88b3e" model_name = "a-model-name" vnf_instantiate_input = VnfInstantiateInput(vnfr_uuid=vnfr_uuid, model_name=model_name) +cloud = "microk8s" +sample_vnfr = { + "_id": "9f472177-95c0-4335-b357-5cdc17a79965", + "id": "9f472177-95c0-4335-b357-5cdc17a79965", + "nsr-id-ref": "dcf4c922-5a73-41bf-a6ca-870c22d6336c", + "vnfd-ref": "jar_vnfd_scalable", + "vnfd-id": "f1b38eac-190c-485d-9a74-c6e169c929d8", + "vim-account-id": "9b0bedc3-ea8e-42fd-acc9-dd03f4dee73c", +} +sample_vnfd = { + "_id": "97784f19-d254-4252-946c-cf92d85443ca", + "id": "sol006-vnf", + "provider": "Canonical", + "product-name": "test-vnf", + "software-version": "1.0", +} +sample_vim_record = { + "_id": "a64f7c6c-bc27-4ec8-b664-5500a3324eca", + "name": "juju", + "vim_type": "paas", + "vim_url": "192.168.1.100:17070", + "vim_user": "admin", + "vim_password": "c16gylWEepEREN6vWw==", + "config": { + "paas_provider": "juju", + "cloud": cloud, + "cloud_credentials": "microk8s", + "authorized_keys": "$HOME/.local/share/juju/ssh/juju_id_rsa.pub", + "ca_cert_content": "-----BEGIN-----", + }, +} class TestVnfDbActivity(asynctest.TestCase): @@ -52,24 +84,6 @@ class TestVnfDbActivity(asynctest.TestCase): ) -sample_vnfr = { - "_id": "9f472177-95c0-4335-b357-5cdc17a79965", - "id": "9f472177-95c0-4335-b357-5cdc17a79965", - "nsr-id-ref": "dcf4c922-5a73-41bf-a6ca-870c22d6336c", - "vnfd-ref": "jar_vnfd_scalable", - "vnfd-id": "f1b38eac-190c-485d-9a74-c6e169c929d8", - "vim-account-id": "9b0bedc3-ea8e-42fd-acc9-dd03f4dee73c", -} - -sample_vnfd = { - "_id": "97784f19-d254-4252-946c-cf92d85443ca", - "id": "sol006-vnf", - "provider": "Canonical", - "product-name": "test-vnf", - "software-version": "1.0", -} - - class TestVnfDetails(asynctest.TestCase): async def setUp(self): self.db = Mock() @@ -120,5 +134,64 @@ class TestVnfDetails(asynctest.TestCase): self.assertEqual(activity_result, None) +class TestGetVimCloud(asynctest.TestCase): + async def setUp(self): + self.db = Mock() + self.vnf_operations_instance = VnfOperations(self.db) + self.env = ActivityEnvironment() + + async def test_activity_succeeded(self): + self.db.get_one.side_effect = [sample_vnfr, sample_vim_record] + activity_result = await self.env.run( + self.vnf_operations_instance.get_vim_cloud, + GetVimCloudInput(vnfr_uuid=sample_vnfr["id"]), + ) + self.assertEqual(activity_result.cloud, cloud) + + async def test_activity_vim_record_without_cloud(self): + vim_record = deepcopy(sample_vim_record) + vim_record["config"].pop("cloud") + self.db.get_one.side_effect = [sample_vnfr, vim_record] + activity_result = await self.env.run( + self.vnf_operations_instance.get_vim_cloud, + GetVimCloudInput(vnfr_uuid=sample_vnfr["id"]), + ) + self.assertEqual(activity_result.cloud, "") + + async def test_activity_failed_db_exception(self): + self.db.get_one.side_effect = DbException("Can not connect to Database.") + with self.assertRaises(DbException) as err: + activity_result = await self.env.run( + self.vnf_operations_instance.get_vim_cloud, + GetVimCloudInput(vnfr_uuid=sample_vnfr["id"]), + ) + self.assertEqual(activity_result, None) + self.assertEqual( + str(err.exception), "database exception Can not connect to Database." + ) + + async def test_activity_failed_key_error(self): + vim_record = deepcopy(sample_vim_record) + vim_record.pop("config") + self.db.get_one.side_effect = [sample_vnfr, vim_record] + with self.assertRaises(KeyError) as err: + activity_result = await self.env.run( + self.vnf_operations_instance.get_vim_cloud, + GetVimCloudInput(vnfr_uuid=sample_vnfr["id"]), + ) + self.assertEqual(activity_result, None) + self.assertEqual(str(err.exception.args[0]), "config") + + async def test_activity_cancelled(self): + self.env._cancelled = True + self.db.get_one.side_effect = [sample_vnfr, sample_vim_record] + with self.assertRaises(CancelledError): + activity_result = await self.env.run( + self.vnf_operations_instance.get_vim_cloud, + GetVimCloudInput(vnfr_uuid=sample_vnfr["id"]), + ) + self.assertEqual(activity_result, None) + + if __name__ == "__main__": asynctest.main() diff --git a/osm_lcm/tests/test_vnf_workflows.py b/osm_lcm/tests/test_vnf_workflows.py index 6fd97d8..2f2c148 100644 --- a/osm_lcm/tests/test_vnf_workflows.py +++ b/osm_lcm/tests/test_vnf_workflows.py @@ -15,12 +15,14 @@ # limitations under the License. import asynctest +from copy import deepcopy from temporalio import activity from temporalio import workflow from temporalio.client import WorkflowFailureError from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker -from unittest.mock import Mock +from unittest import TestCase +from unittest.mock import Mock, patch, AsyncMock from osm_common.dataclasses.temporal_dataclasses import ( ChangeVnfInstantiationStateInput, @@ -28,6 +30,8 @@ from osm_common.dataclasses.temporal_dataclasses import ( CharmInfo, GetTaskQueueInput, GetTaskQueueOutput, + GetVimCloudInput, + GetVimCloudOutput, GetVnfDetailsInput, GetVnfDetailsOutput, VduComputeConstraints, @@ -40,6 +44,7 @@ from osm_common.temporal_constants import ( ACTIVITY_CHANGE_VNF_STATE, ACTIVITY_CHANGE_VNF_INSTANTIATION_STATE, ACTIVITY_GET_TASK_QUEUE, + ACTIVITY_GET_VIM_CLOUD, ACTIVITY_GET_VNF_DETAILS, ACTIVITY_SEND_NOTIFICATION_FOR_VNF, ACTIVITY_SET_VNF_MODEL, @@ -47,22 +52,27 @@ from osm_common.temporal_constants import ( WORKFLOW_VDU_INSTANTIATE, WORKFLOW_VNF_PREPARE, ) + from osm_lcm.temporal.vnf_workflows import VnfInstantiateWorkflow, VnfPrepareWorkflow +# The variables used in the tests +model_name = "my-model-name" wf_input = VnfInstantiateInput( vnfr_uuid="86b53d92-4f5a-402e-8ac2-585ec6b64698", - model_name="a-model-name", + model_name=model_name, ) cloud = "microk8s" juju_task_queue = "juju_task_queue" vnfr_uuid = "9f472177-95c0-4335-b357-5cdc17a79965" +vim_account_id = "9b0bedc3-ea8e-42fd-acc9-dd03f4dee73c" sample_vnfr = { "_id": vnfr_uuid, "id": vnfr_uuid, "nsr-id-ref": "dcf4c922-5a73-41bf-a6ca-870c22d6336c", "vnfd-ref": "jar_vnfd_scalable", "vnfd-id": "f1b38eac-190c-485d-9a74-c6e169c929d8", - "vim-account-id": "9b0bedc3-ea8e-42fd-acc9-dd03f4dee73c", + "vim-account-id": vim_account_id, + "namespace": model_name, } vdu = { "id": "hackfest_basic-VM", @@ -72,6 +82,19 @@ vdu = { "virtual-storage-desc": ["hackfest_basic-VM-storage"], } vnfd_id = "97784f19-d254-4252-946c-cf92d85443ca" +flavor_1 = { + "id": "compute-id-1", + "virtual-memory": {"size": "4"}, + "virtual-cpu": {"cpu-architecture": "x86", "num-virtual-cpu": 2}, +} +flavor_2 = { + "id": "compute-id-2", + "virtual-cpu": {"cpu-architecture": "x86", "num-virtual-cpu": 2}, +} +flavor_3 = { + "id": "compute-id-2", + "virtual-memory": {"size": "4"}, +} sample_vnfd = { "_id": vnfd_id, "id": "sol006-vnf", @@ -79,7 +102,17 @@ sample_vnfd = { "product-name": "test-vnf", "software-version": "1.0", "vdu": [vdu], + "virtual-compute-desc": [flavor_1, flavor_2], } +sample_charm_info = CharmInfo(app_name="my-app", channel="latest", entity_url="my-url") +sample_constraints = VduComputeConstraints(cores=1, mem=1) +sample_vdu_instantiate_input = VduInstantiateInput( + vim_uuid=vim_account_id, + model_name=model_name, + charm_info=sample_charm_info, + constraints=sample_constraints, + cloud=cloud, +) SANDBOXED = False DEBUG_MODE = True @@ -143,6 +176,20 @@ async def mock_get_vnf_details_raise_exception( raise TestException(f"{ACTIVITY_GET_VNF_DETAILS} failed.") +@activity.defn(name=ACTIVITY_GET_VIM_CLOUD) +async def mock_get_vim_cloud( + get_vim_cloud_input: GetVimCloudInput, +) -> GetVimCloudOutput: + return GetVimCloudOutput(cloud=cloud) + + +@activity.defn(name=ACTIVITY_GET_VIM_CLOUD) +async def mock_get_vim_cloud_raise_exception( + get_vim_cloud_input: GetVimCloudInput, +) -> GetVimCloudOutput: + raise TestException(f"{ACTIVITY_GET_VIM_CLOUD} failed.") + + # Mock Workflowa @workflow.defn(name=WORKFLOW_VNF_PREPARE, sandboxed=SANDBOXED) class MockPrepareVnfWorkflow: @@ -317,6 +364,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -325,7 +373,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.check_state_change_call_counts() self.workflow_is_succeeded() mock_instantiate_vdus.assert_called_once_with( - sample_vnfr, sample_vnfd, LCM_TASK_QUEUE + vnfr=sample_vnfr, vnfd=sample_vnfd, task_queue=LCM_TASK_QUEUE, cloud=cloud ) @asynctest.mock.patch( @@ -341,6 +389,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -379,6 +428,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -408,6 +458,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details_empty_output, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -415,7 +466,9 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): await self.execute_workflow() self.check_state_change_call_counts() self.workflow_is_succeeded() - mock_instantiate_vdus.assert_called_once_with({}, {}, LCM_TASK_QUEUE) + mock_instantiate_vdus.assert_called_once_with( + vnfr={}, vnfd={}, task_queue=LCM_TASK_QUEUE, cloud=cloud + ) @asynctest.mock.patch( "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.instantiate_vdus" @@ -430,6 +483,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -458,6 +512,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details_raise_exception, + mock_get_vim_cloud, ] async with self.env, self.get_worker(self.task_queue, workflows, activities): with self.assertRaises(WorkflowFailureError) as err: @@ -470,6 +525,33 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): "get_vnf_details failed.", ) + @asynctest.mock.patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.instantiate_vdus" + ) + async def test_vnf_instantiate_workflow_get_vim_cloud_raises_exception( + self, mock_instantiate_vdus + ): + workflows = [VnfInstantiateWorkflow, MockPrepareVnfWorkflow] + activities = [ + mock_get_task_queue, + self.mock_change_vnf_instantiation_state, + self.mock_change_vnf_state, + self.mock_send_notification_for_vnf, + mock_set_vnf_model, + mock_get_vnf_details, + mock_get_vim_cloud_raise_exception, + ] + async with self.env, self.get_worker(self.task_queue, workflows, activities): + with self.assertRaises(WorkflowFailureError) as err: + await self.execute_workflow() + self.check_state_change_call_counts() + self.workflow_is_failed(err) + mock_instantiate_vdus.assert_not_called() + self.assertEqual( + str(err.exception.cause.cause.message), + "get-vim-cloud failed.", + ) + @asynctest.mock.patch( "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_vdu_instantiate_input" ) @@ -477,16 +559,8 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self, mock_vdu_instantiate_input ): mock_vdu_instantiate_input.return_value = ( - VduInstantiateInput( - vim_uuid="123", - model_name="a-model-name", - charm_info=CharmInfo( - app_name="my-app", channel="latest", entity_url="my-url" - ), - constraints=VduComputeConstraints(cores=1, mem=1), - cloud=cloud, - ), - "vdu_instantiate_workflow_id", + sample_vdu_instantiate_input, + "vdu-instantiate-workflow-id", ) activities = [ mock_get_task_queue, @@ -495,6 +569,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, self.workflows, activities @@ -502,10 +577,93 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): await self.execute_workflow() self.check_state_change_call_counts() self.workflow_is_succeeded() - call_mock_vdu_instantiate_input = mock_vdu_instantiate_input.call_args - self.assertEqual(call_mock_vdu_instantiate_input.args[0], sample_vnfr) - self.assertEqual(call_mock_vdu_instantiate_input.args[1], sample_vnfd) - self.assertEqual(call_mock_vdu_instantiate_input.args[2], vdu) + call_mock_vdu_instantiate_input = mock_vdu_instantiate_input.call_args_list + self.assertEqual(call_mock_vdu_instantiate_input[0][1]["vnfr"], sample_vnfr) + self.assertEqual(call_mock_vdu_instantiate_input[0][1]["vnfd"], sample_vnfd) + self.assertEqual(call_mock_vdu_instantiate_input[0][1]["vdu"], vdu) + + @asynctest.mock.patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_vdu_instantiate_input" + ) + @asynctest.mock.patch("temporalio.workflow.execute_child_workflow") + async def test_instantiate_vdus_single_vdu( + self, mock_execute_child_workflow: AsyncMock, mock_get_vdu_instantiate_input + ): + mock_get_vdu_instantiate_input.return_value = ( + sample_vdu_instantiate_input, + "vdu-instantiate-workflow-id", + ) + await VnfInstantiateWorkflow.instantiate_vdus( + sample_vnfr, sample_vnfd, LCM_TASK_QUEUE, cloud + ) + self.assertEqual(mock_execute_child_workflow.call_count, 1) + mock_execute_child_workflow.assert_called_once_with( + workflow=WORKFLOW_VDU_INSTANTIATE, + arg=sample_vdu_instantiate_input, + task_queue=LCM_TASK_QUEUE, + id="vdu-instantiate-workflow-id", + ) + mock_get_vdu_instantiate_input.assert_called_once_with( + vnfr=sample_vnfr, vnfd=sample_vnfd, vdu=vdu, cloud=cloud + ) + + @asynctest.mock.patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_vdu_instantiate_input" + ) + @asynctest.mock.patch("temporalio.workflow.execute_child_workflow") + async def test_instantiate_vdus_multiple_vdus( + self, mock_execute_child_workflow: AsyncMock, mock_get_vdu_instantiate_input + ): + sample_vnfd_multi_vdu = deepcopy(sample_vnfd) + sample_vnfd_multi_vdu["vdu"] = [vdu, vdu] + mock_get_vdu_instantiate_input.side_effect = [ + (sample_vdu_instantiate_input, "vdu-instantiate-workflow-id-1"), + (sample_vdu_instantiate_input, "vdu-instantiate-workflow-id-2"), + ] + await VnfInstantiateWorkflow.instantiate_vdus( + sample_vnfr, sample_vnfd_multi_vdu, LCM_TASK_QUEUE, cloud + ) + self.assertEqual(mock_execute_child_workflow.call_count, 2) + call_mock_execute_child_workflow = mock_execute_child_workflow.call_args_list + self.assertEqual( + call_mock_execute_child_workflow[0][1], + { + "arg": sample_vdu_instantiate_input, + "id": "vdu-instantiate-workflow-id-1", + "task_queue": "lcm-task-queue", + "workflow": "vdu-instantiate", + }, + ) + self.assertEqual( + call_mock_execute_child_workflow[1][1], + { + "arg": sample_vdu_instantiate_input, + "id": "vdu-instantiate-workflow-id-2", + "task_queue": "lcm-task-queue", + "workflow": "vdu-instantiate", + }, + ) + call_mock_get_vdu_instantiate_input = ( + mock_get_vdu_instantiate_input.call_args_list + ) + self.assertEqual( + call_mock_get_vdu_instantiate_input[0][1], + { + "vnfr": sample_vnfr, + "vnfd": sample_vnfd_multi_vdu, + "vdu": vdu, + "cloud": cloud, + }, + ) + self.assertEqual( + call_mock_get_vdu_instantiate_input[1][1], + { + "vnfr": sample_vnfr, + "vnfd": sample_vnfd_multi_vdu, + "vdu": vdu, + "cloud": cloud, + }, + ) @asynctest.mock.patch( "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.instantiate_vdus" @@ -521,6 +679,7 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.mock_send_notification_for_vnf, mock_set_vnf_model, mock_get_vnf_details, + mock_get_vim_cloud, ] async with self.env, self.get_worker( self.task_queue, workflows, activities @@ -529,8 +688,192 @@ class TestVnfInstantiateWorkflow(asynctest.TestCase): self.check_state_change_call_counts() self.workflow_is_succeeded() mock_instantiate_vdus.assert_called_once_with( - sample_vnfr, sample_vnfd, juju_task_queue + vnfr=sample_vnfr, vnfd=sample_vnfd, task_queue=juju_task_queue, cloud=cloud + ) + + +class TestGetVduInstantiateInputMethods(TestCase): + def test_get_flavor_details_successful(self): + compute_desc_id = "compute-id-1" + result = VnfInstantiateWorkflow.get_flavor_details(compute_desc_id, sample_vnfd) + self.assertEqual(result, flavor_1) + + def test_get_flavor_details_empty_compute_desc(self): + compute_desc_id = "" + result = VnfInstantiateWorkflow.get_flavor_details(compute_desc_id, sample_vnfd) + self.assertEqual(result, {}) + + def test_get_flavor_details_compute_desc_not_found(self): + compute_desc_id = "compute-id-5" + result = VnfInstantiateWorkflow.get_flavor_details(compute_desc_id, sample_vnfd) + self.assertEqual(result, {}) + + def test_get_flavor_details_empty_vnfd(self): + compute_desc_id = "compute-id-5" + result = VnfInstantiateWorkflow.get_flavor_details(compute_desc_id, {}) + self.assertEqual(result, {}) + + def test_get_flavor_details_wrong_vnfd_format(self): + compute_desc_id = "compute-id-2" + sample_vnfd = { + "_id": vnfd_id, + "vdu": [vdu], + "virtual-compute-desc": [ + { + "virtual-memory": {"size": "4"}, + } + ], + } + result = VnfInstantiateWorkflow.get_flavor_details(compute_desc_id, sample_vnfd) + self.assertEqual(result, {}) + + @patch("osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_flavor_details") + def test_get_compute_constraints(self, mock_get_flavor_details): + mock_get_flavor_details.return_value = flavor_1 + result = VnfInstantiateWorkflow.get_compute_constraints(vdu, sample_vnfd) + self.assertEqual(VduComputeConstraints(cores=2, mem=4), result) + + @patch("osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_flavor_details") + def test_get_compute_constraints_empty_flavor_details( + self, mock_get_flavor_details + ): + mock_get_flavor_details.return_value = {} + result = VnfInstantiateWorkflow.get_compute_constraints(vdu, sample_vnfd) + self.assertEqual(VduComputeConstraints(cores=0, mem=0), result) + + @patch("osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_flavor_details") + def test_get_compute_constraints_flavor_details_is_none( + self, mock_get_flavor_details + ): + mock_get_flavor_details.return_value = None + result = VnfInstantiateWorkflow.get_compute_constraints(vdu, sample_vnfd) + self.assertEqual(VduComputeConstraints(cores=0, mem=0), result) + + @patch("osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_flavor_details") + def test_get_compute_constraints_flavor_has_only_cpu(self, mock_get_flavor_details): + mock_get_flavor_details.return_value = flavor_2 + result = VnfInstantiateWorkflow.get_compute_constraints(vdu, sample_vnfd) + self.assertEqual(VduComputeConstraints(cores=2, mem=0), result) + + @patch("osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_flavor_details") + def test_get_compute_constraints_flavor_has_only_memory( + self, mock_get_flavor_details + ): + mock_get_flavor_details.return_value = flavor_3 + result = VnfInstantiateWorkflow.get_compute_constraints(vdu, sample_vnfd) + self.assertEqual(VduComputeConstraints(cores=0, mem=4), result) + + @patch("osm_lcm.temporal.juju_paas_activities.CharmInfoUtils.get_charm_info") + @patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_compute_constraints" + ) + def test_get_vdu_instantiate_input( + self, mock_get_compute_constraints, mock_get_charm_info + ): + mock_get_compute_constraints.return_value = sample_constraints + mock_get_charm_info.return_value = sample_charm_info + ( + vdu_instantiate_input, + vdu_instantiate_wf_id, + ) = VnfInstantiateWorkflow.get_vdu_instantiate_input( + sample_vnfr, sample_vnfd, vdu, cloud + ) + self.assertEqual(vdu_instantiate_input, sample_vdu_instantiate_input) + self.assertEqual(vdu_instantiate_wf_id, "my-model-name-my-app") + mock_get_charm_info.assert_called_once() + mock_get_compute_constraints.assert_called_once() + + @patch("osm_lcm.temporal.juju_paas_activities.CharmInfoUtils.get_charm_info") + @patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_compute_constraints" + ) + def test_get_vdu_instantiate_input_vnfr_without_namespace( + self, mock_get_compute_constraints, mock_get_charm_info + ): + vnfr = deepcopy(sample_vnfr) + vnfr.pop("namespace") + mock_get_compute_constraints.return_value = sample_constraints + mock_get_charm_info.return_value = sample_charm_info + with self.assertRaises(TypeError) as err: + VnfInstantiateWorkflow.get_vdu_instantiate_input( + vnfr, sample_vnfd, vdu, cloud + ) + self.assertEqual( + str(err.exception), + "unsupported operand type(s) for +: 'NoneType' and 'str'", + ) + mock_get_charm_info.assert_called_once() + mock_get_compute_constraints.assert_called_once() + + @patch("osm_lcm.temporal.juju_paas_activities.CharmInfoUtils.get_charm_info") + @patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_compute_constraints" + ) + def test_get_vdu_instantiate_input_app_name_is_empty( + self, mock_get_compute_constraints, mock_get_charm_info + ): + mock_get_compute_constraints.return_value = sample_constraints + charm_info = CharmInfo(app_name="", channel="latest", entity_url="my-url") + mock_get_charm_info.return_value = charm_info + ( + vdu_instantiate_input, + vdu_instantiate_wf_id, + ) = VnfInstantiateWorkflow.get_vdu_instantiate_input( + sample_vnfr, sample_vnfd, vdu, cloud + ) + self.assertEqual( + vdu_instantiate_input, + VduInstantiateInput( + vim_uuid=vim_account_id, + model_name=model_name, + charm_info=charm_info, + constraints=sample_constraints, + cloud=cloud, + ), + ) + self.assertEqual(vdu_instantiate_wf_id, "my-model-name-") + mock_get_charm_info.assert_called_once() + mock_get_compute_constraints.assert_called_once() + + @patch("osm_lcm.temporal.juju_paas_activities.CharmInfoUtils.get_charm_info") + @patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_compute_constraints" + ) + def test_get_vdu_instantiate_input_get_compute_constraints_raises( + self, mock_get_compute_constraints, mock_get_charm_info + ): + mock_get_compute_constraints.side_effect = TestException( + "get_compute_constraints failed" + ) + mock_get_charm_info.return_value = sample_charm_info + with self.assertRaises(TestException) as err: + result = VnfInstantiateWorkflow.get_vdu_instantiate_input( + sample_vnfr, sample_vnfd, vdu, cloud + ) + self.assertEqual(result, None) + self.assertEqual(str(err.exception), "get_compute_constraints failed") + mock_get_charm_info.assert_called_once() + mock_get_compute_constraints.assert_called_once() + + @patch("osm_lcm.temporal.juju_paas_activities.CharmInfoUtils.get_charm_info") + @patch( + "osm_lcm.temporal.vnf_workflows.VnfInstantiateWorkflow.get_compute_constraints" + ) + def test_get_vdu_instantiate_input_get_charm_info_raises( + self, mock_get_compute_constraints, mock_get_charm_info + ): + mock_get_compute_constraints.return_value = sample_constraints + mock_get_charm_info.side_effect = TestException( + "get_compute_constraints failed" ) + with self.assertRaises(TestException) as err: + result = VnfInstantiateWorkflow.get_vdu_instantiate_input( + sample_vnfr, sample_vnfd, vdu, cloud + ) + self.assertEqual(result, None) + self.assertEqual(str(err.exception), "get_compute_constraints failed") + mock_get_charm_info.assert_called_once() + mock_get_compute_constraints.assert_not_called() if __name__ == "__main__": -- 2.25.1