From: Patricia Reinoso Date: Wed, 12 Apr 2023 15:54:43 +0000 (+0000) Subject: OSMENG-992 - Implement create model activity X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=911946db8fab0a46c2449dcdc21d48e7f0aa83ef;p=osm%2FLCM.git OSMENG-992 - Implement create model activity in NS Workflow. An exception is raised if model already exists Change-Id: I84dd89850b28287dfefb1abc0d158cc72cd4eb34 Signed-off-by: Patricia Reinoso Signed-off-by: Mark Beierl --- diff --git a/osm_lcm/nglcm.py b/osm_lcm/nglcm.py index 4c21d6e..6627bc8 100644 --- a/osm_lcm/nglcm.py +++ b/osm_lcm/nglcm.py @@ -152,13 +152,14 @@ class NGLcm: VnfInstantiateWorkflow, ] activities = [ + ns_data_activity_instance.get_model_info, ns_data_activity_instance.prepare_vnf_records, ns_data_activity_instance.update_ns_state, ns_operation_instance.check_ns_instantiate_finished, ns_operation_instance.deploy_ns, nslcm_activity_instance.update_ns_lcm_operation_state, nslcm_activity_instance.no_op, - paas_connector_instance.create_model_if_doesnt_exist, + paas_connector_instance.create_model, paas_connector_instance.deploy_charm, paas_connector_instance.check_charm_status, paas_connector_instance.test_vim_connectivity, diff --git a/osm_lcm/temporal/juju_paas_activities.py b/osm_lcm/temporal/juju_paas_activities.py index ecd59e6..3c61ec7 100644 --- a/osm_lcm/temporal/juju_paas_activities.py +++ b/osm_lcm/temporal/juju_paas_activities.py @@ -18,15 +18,15 @@ from temporalio import activity from n2vc.temporal_libjuju import ConnectionInfo, Libjuju from osm_common.temporal_constants import ( ACTIVITY_TEST_VIM_CONNECTIVITY, - ACTIVITY_CREATE_MODEL_IF_DOESNT_EXIST, + ACTIVITY_CREATE_MODEL, ACTIVITY_DEPLOY_CHARM, ACTIVITY_CHECK_CHARM_STATUS, ) from osm_common.dataclasses.temporal_dataclasses import ( + CharmInfo, + ModelInfo, TestVimConnectivityInput, - CreateModelInput, VduInstantiateInput, - CharmInfo, ) @@ -116,19 +116,17 @@ class JujuPaasConnector: finally: await libjuju.disconnect_controller(controller) - @activity.defn(name=ACTIVITY_CREATE_MODEL_IF_DOESNT_EXIST) - async def create_model_if_doesnt_exist( - self, create_model_input: CreateModelInput - ) -> None: - # TODO: OSM-991 - """Connects to Juju Controller. Create a new model if model_name does not exist + @activity.defn(name=ACTIVITY_CREATE_MODEL) + async def create_model(self, create_model_input: ModelInfo) -> None: + """Connects to Juju Controller. Creates a new model. Collaborators: DB Read: vim_accounts Juju Controller: Connect and create model. Raises (Retryable): - ApplicationError If Juju controller is not reachable + ApplicationError If Juju controller is not reachable. + If the model already exists. Activity Lifecycle: This activity should complete relatively quickly (in a few seconds). diff --git a/osm_lcm/temporal/ns_activities.py b/osm_lcm/temporal/ns_activities.py index 1a2925c..6834eec 100644 --- a/osm_lcm/temporal/ns_activities.py +++ b/osm_lcm/temporal/ns_activities.py @@ -18,12 +18,14 @@ import logging from time import time from temporalio import activity from osm_common.temporal_constants import ( - ACTIVITY_UPDATE_NS_STATE, ACTIVITY_CHECK_NS_INSTANTIATION_FINISHED, - ACTIVITY_PREPARE_VNF_RECORDS, ACTIVITY_DEPLOY_NS, + ACTIVITY_GET_MODEL_INFO, + ACTIVITY_PREPARE_VNF_RECORDS, + ACTIVITY_UPDATE_NS_STATE, ) from osm_common.dataclasses.temporal_dataclasses import ( + ModelInfo, NsInstantiateInput, UpdateNsStateInput, VduInstantiateInput, @@ -45,7 +47,7 @@ class NsOperations: async def deploy_vnf(self, vnfr: dict): vim_id = vnfr.get("vim-account-id") - model_name = vnfr.get("namespace") + model_name = "model-name" vnfd_id = vnfr["vnfd-id"] vnfd = self.db.get_one("vnfds", {"_id": vnfd_id}) sw_image_descs = vnfd.get("sw-image-desc") @@ -106,6 +108,32 @@ class NsDbActivity: for vnfr in vnfrs: self._prepare_vnf_record(vnfr) + @activity.defn(name=ACTIVITY_GET_MODEL_INFO) + async def get_model_info( + self, ns_instantiate_input: NsInstantiateInput + ) -> ModelInfo: + """Returns a ModelInfo. Contains VIM ID and model name. + + Collaborators: + DB Read: nsrs + + Raises (Retryable): + DbException If the target DB record does not exist or DB is not reachable. + + Activity Lifecycle: + This activity will not report a heartbeat due to its + short-running nature. + + As this is a direct DB update, it is not recommended to have + any specific retry policy + + """ + ns_uuid = ns_instantiate_input.ns_uuid + nsr = self.db.get_one("nsrs", {"_id": ns_uuid}) + vim_uuid = nsr.get("datacenter") + model_name = self._get_namespace(ns_uuid, vim_uuid) + return ModelInfo(vim_uuid, model_name) + def _get_namespace(self, ns_id: str, vim_id: str) -> str: """The NS namespace is the combination if the NS ID and the VIM ID.""" return ns_id[-12:] + "-" + vim_id[-12:] diff --git a/osm_lcm/temporal/ns_workflows.py b/osm_lcm/temporal/ns_workflows.py index bf0be65..bef6766 100644 --- a/osm_lcm/temporal/ns_workflows.py +++ b/osm_lcm/temporal/ns_workflows.py @@ -15,20 +15,24 @@ # limitations under the License. from temporalio import workflow +from temporalio.converter import value_to_type from temporalio.exceptions import ActivityError from osm_common.dataclasses.temporal_dataclasses import ( + ModelInfo, NsInstantiateInput, NsLcmOperationInput, NsState, UpdateNsStateInput, ) from osm_common.temporal_constants import ( - WORKFLOW_NS_INSTANTIATE, - ACTIVITY_UPDATE_NS_STATE, + ACTIVITY_CREATE_MODEL, ACTIVITY_CHECK_NS_INSTANTIATION_FINISHED, ACTIVITY_DEPLOY_NS, + ACTIVITY_GET_MODEL_INFO, + ACTIVITY_UPDATE_NS_STATE, LCM_TASK_QUEUE, + WORKFLOW_NS_INSTANTIATE, ) from osm_lcm.temporal.lcm_workflows import LcmOperationWorkflow @@ -51,7 +55,26 @@ class NsInstantiateWorkflow(LcmOperationWorkflow): ns_state = UpdateNsStateInput(input.ns_uuid, NsState.INSTANTIATED, "Done") try: - # TODO: Create the model here OSM-991 + model_info = value_to_type( + ModelInfo, + await workflow.execute_activity( + activity=ACTIVITY_GET_MODEL_INFO, + arg=input, + activity_id=f"{ACTIVITY_GET_MODEL_INFO}-{input.ns_uuid}", + task_queue=LCM_TASK_QUEUE, + schedule_to_close_timeout=LcmOperationWorkflow.default_schedule_to_close_timeout, + retry_policy=LcmOperationWorkflow.no_retry_policy, + ), + ) + + await workflow.execute_activity( + activity=ACTIVITY_CREATE_MODEL, + arg=model_info, + activity_id=f"{ACTIVITY_CREATE_MODEL}-{input.ns_uuid}", + task_queue=LCM_TASK_QUEUE, + schedule_to_close_timeout=LcmOperationWorkflow.default_schedule_to_close_timeout, + retry_policy=LcmOperationWorkflow.no_retry_policy, + ) # TODO: Change this to a workflow OSM-990 await workflow.execute_activity( diff --git a/osm_lcm/temporal/tests/test_charm_info_utils.py b/osm_lcm/temporal/tests/test_charm_info_utils.py deleted file mode 100644 index fd1cd9b..0000000 --- a/osm_lcm/temporal/tests/test_charm_info_utils.py +++ /dev/null @@ -1,158 +0,0 @@ -# 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. - -from unittest import TestCase -from osm_lcm.temporal.juju_paas_activities import CharmInfoUtils -from osm_common.dataclasses.temporal_dataclasses import CharmInfo -import yaml - -nsr_id = "ea958ba5-4e58-4405-bf42-6e3be15d4c3a" -vim_id = "70b47595-fafa-4f63-904b-fc3ada60eebb" -expected_default_ns_model = "6e3be15d4c3a-fc3ada60eebb" - -vdu_nominal = """ ---- -vdu: - - id: test-vdu-id - name: test-vdu-name - int-cpd: - - id: internal - int-virtual-link-desc: network1 - - id: mgmt - virtual-compute-desc: compute-id - virtual-storage-desc: - - storage-id - sw-image-desc: image-test2 - configurable-properties: - - key: "track" - value: "latest" - - key: "channel" - value: "edge" -""" - -vdu_no_channel = """ ---- -vdu: - - id: test-vdu-id - name: test-vdu-name - int-cpd: - - id: internal - int-virtual-link-desc: network1 - - id: mgmt - virtual-compute-desc: compute-id - virtual-storage-desc: - - storage-id - sw-image-desc: image-test2 - configurable-properties: - - key: "track" - value: "latest" - - key: "key" - value: "edge" -""" - -vdu_invalid_image = """ ---- -vdu: - - id: test-vdu-id - name: test-vdu-name - int-cpd: - - id: internal - int-virtual-link-desc: network1 - - id: mgmt - virtual-compute-desc: compute-id - virtual-storage-desc: - - storage-id - sw-image-desc: invalid_image - configurable-properties: - - key: "track" - value: "latest" - - key: "key" - value: "edge" -""" - -vdu_no_sw_image_desc = """ ---- -vdu: - - id: test-vdu-id - name: test-vdu-name - int-cpd: - - id: internal - int-virtual-link-desc: network1 - - id: mgmt - virtual-compute-desc: compute-id - virtual-storage-desc: - - storage-id - sw-image-desc: invalid_image - configurable-properties: - - key: "track" - value: "latest" - - key: "key" - value: "edge" -""" - -sw_image_desc = """ ---- -sw-image-desc: - - id: image-test1 - name: charm-name1 - image: ch:mysql - version: "1.0" - - id: image-test2 - name: charm-name2 - image: ch:my-charm - version: "1.0" -""" - - -class TestCharmInfoUtils(TestCase): - def setUp(self): - self.charm_info_utils = CharmInfoUtils() - - def get_loaded_descriptor(self, descriptor): - return yaml.load(descriptor, Loader=yaml.Loader) - - def test_get_charm_info_nominal_case(self): - vdu_descriptor = self.get_loaded_descriptor(vdu_nominal).get("vdu") - sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") - result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) - expected = CharmInfo("test-vdu-id", "edge", "ch:my-charm") - self.assertEqual(result, expected) - - def test_get_charm_info_no_channel(self): - vdu_descriptor = self.get_loaded_descriptor(vdu_no_channel).get("vdu") - sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") - result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) - expected = CharmInfo("test-vdu-id", None, "ch:my-charm") - self.assertEqual(result, expected) - - def test_get_charm_info_invalid_image(self): - vdu_descriptor = self.get_loaded_descriptor(vdu_invalid_image).get("vdu") - sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") - result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) - expected = CharmInfo("test-vdu-id", None, None) - self.assertEqual(result, expected) - - def test_get_charm_info_no_sw_image_desc(self): - vdu_descriptor = self.get_loaded_descriptor(vdu_no_sw_image_desc).get("vdu") - sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") - result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) - expected = CharmInfo("test-vdu-id", None, None) - self.assertEqual(result, expected) - - def test_get_charm_info_empty_sw_image_descs(self): - vdu_descriptor = self.get_loaded_descriptor(vdu_nominal).get("vdu") - sw_image_descs = [] - result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) - expected = CharmInfo("test-vdu-id", "edge", None) - self.assertEqual(result, expected) diff --git a/osm_lcm/tests/test_charm_info_utils.py b/osm_lcm/tests/test_charm_info_utils.py new file mode 100644 index 0000000..fd1cd9b --- /dev/null +++ b/osm_lcm/tests/test_charm_info_utils.py @@ -0,0 +1,158 @@ +# 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. + +from unittest import TestCase +from osm_lcm.temporal.juju_paas_activities import CharmInfoUtils +from osm_common.dataclasses.temporal_dataclasses import CharmInfo +import yaml + +nsr_id = "ea958ba5-4e58-4405-bf42-6e3be15d4c3a" +vim_id = "70b47595-fafa-4f63-904b-fc3ada60eebb" +expected_default_ns_model = "6e3be15d4c3a-fc3ada60eebb" + +vdu_nominal = """ +--- +vdu: + - id: test-vdu-id + name: test-vdu-name + int-cpd: + - id: internal + int-virtual-link-desc: network1 + - id: mgmt + virtual-compute-desc: compute-id + virtual-storage-desc: + - storage-id + sw-image-desc: image-test2 + configurable-properties: + - key: "track" + value: "latest" + - key: "channel" + value: "edge" +""" + +vdu_no_channel = """ +--- +vdu: + - id: test-vdu-id + name: test-vdu-name + int-cpd: + - id: internal + int-virtual-link-desc: network1 + - id: mgmt + virtual-compute-desc: compute-id + virtual-storage-desc: + - storage-id + sw-image-desc: image-test2 + configurable-properties: + - key: "track" + value: "latest" + - key: "key" + value: "edge" +""" + +vdu_invalid_image = """ +--- +vdu: + - id: test-vdu-id + name: test-vdu-name + int-cpd: + - id: internal + int-virtual-link-desc: network1 + - id: mgmt + virtual-compute-desc: compute-id + virtual-storage-desc: + - storage-id + sw-image-desc: invalid_image + configurable-properties: + - key: "track" + value: "latest" + - key: "key" + value: "edge" +""" + +vdu_no_sw_image_desc = """ +--- +vdu: + - id: test-vdu-id + name: test-vdu-name + int-cpd: + - id: internal + int-virtual-link-desc: network1 + - id: mgmt + virtual-compute-desc: compute-id + virtual-storage-desc: + - storage-id + sw-image-desc: invalid_image + configurable-properties: + - key: "track" + value: "latest" + - key: "key" + value: "edge" +""" + +sw_image_desc = """ +--- +sw-image-desc: + - id: image-test1 + name: charm-name1 + image: ch:mysql + version: "1.0" + - id: image-test2 + name: charm-name2 + image: ch:my-charm + version: "1.0" +""" + + +class TestCharmInfoUtils(TestCase): + def setUp(self): + self.charm_info_utils = CharmInfoUtils() + + def get_loaded_descriptor(self, descriptor): + return yaml.load(descriptor, Loader=yaml.Loader) + + def test_get_charm_info_nominal_case(self): + vdu_descriptor = self.get_loaded_descriptor(vdu_nominal).get("vdu") + sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") + result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) + expected = CharmInfo("test-vdu-id", "edge", "ch:my-charm") + self.assertEqual(result, expected) + + def test_get_charm_info_no_channel(self): + vdu_descriptor = self.get_loaded_descriptor(vdu_no_channel).get("vdu") + sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") + result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) + expected = CharmInfo("test-vdu-id", None, "ch:my-charm") + self.assertEqual(result, expected) + + def test_get_charm_info_invalid_image(self): + vdu_descriptor = self.get_loaded_descriptor(vdu_invalid_image).get("vdu") + sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") + result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) + expected = CharmInfo("test-vdu-id", None, None) + self.assertEqual(result, expected) + + def test_get_charm_info_no_sw_image_desc(self): + vdu_descriptor = self.get_loaded_descriptor(vdu_no_sw_image_desc).get("vdu") + sw_image_descs = self.get_loaded_descriptor(sw_image_desc).get("sw-image-desc") + result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) + expected = CharmInfo("test-vdu-id", None, None) + self.assertEqual(result, expected) + + def test_get_charm_info_empty_sw_image_descs(self): + vdu_descriptor = self.get_loaded_descriptor(vdu_nominal).get("vdu") + sw_image_descs = [] + result = self.charm_info_utils.get_charm_info(vdu_descriptor[0], sw_image_descs) + expected = CharmInfo("test-vdu-id", "edge", None) + self.assertEqual(result, expected) diff --git a/osm_lcm/tests/test_juju_paas_activities.py b/osm_lcm/tests/test_juju_paas_activities.py new file mode 100644 index 0000000..7d94a2c --- /dev/null +++ b/osm_lcm/tests/test_juju_paas_activities.py @@ -0,0 +1,77 @@ +####################################################################################### +# 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. + +import asynctest +from juju.errors import JujuError +from osm_common.dataclasses.temporal_dataclasses import ModelInfo +from osm_common.dbbase import DbException +from osm_lcm.temporal.juju_paas_activities import JujuPaasConnector +from n2vc.temporal_libjuju import ConnectionInfo +from temporalio.testing import ActivityEnvironment +from unittest.mock import Mock + +vim_id = "some-vim-uuid" +namespace = "some-namespace" +model_info = ModelInfo(vim_id, namespace) +connection_info = ConnectionInfo( + "1.2.3.4:17070", "user", "password", "cacert", "cloud_name", "cloud_credentials" +) + + +class TestJujuPaasConnector(asynctest.TestCase): + def setUp(self): + self.db = Mock() + self.env = ActivityEnvironment() + self.juju_paas_connector = JujuPaasConnector(self.db) + + @asynctest.mock.patch("osm_lcm.temporal.juju_paas_activities.Libjuju.add_model") + @asynctest.mock.patch( + "osm_lcm.temporal.juju_paas_activities.JujuPaasConnector._get_connection_info" + ) + async def test_create_model_nominal_case( + self, mock_get_connection_info, mock_add_model + ): + mock_get_connection_info.return_value = connection_info + await self.env.run(self.juju_paas_connector.create_model, model_info) + mock_get_connection_info.assert_called_once_with(vim_id) + mock_add_model.assert_called_once_with(namespace) + + @asynctest.mock.patch("osm_lcm.temporal.juju_paas_activities.Libjuju.add_model") + @asynctest.mock.patch( + "osm_lcm.temporal.juju_paas_activities.JujuPaasConnector._get_connection_info" + ) + async def test_create_model_raises_juju_exception( + self, mock_get_connection_info, mock_add_model + ): + mock_get_connection_info.return_value = connection_info + mock_add_model.side_effect = JujuError() + with self.assertRaises(JujuError): + await self.env.run(self.juju_paas_connector.create_model, model_info) + mock_get_connection_info.assert_called_once_with(vim_id) + mock_add_model.assert_called_once_with(namespace) + + @asynctest.mock.patch("osm_lcm.temporal.juju_paas_activities.Libjuju.add_model") + @asynctest.mock.patch( + "osm_lcm.temporal.juju_paas_activities.JujuPaasConnector._get_connection_info" + ) + async def test_create_model_raises_db_exception( + self, mock_get_connection_info, mock_add_model + ): + mock_get_connection_info.side_effect = DbException("not found") + with self.assertRaises(DbException): + await self.env.run(self.juju_paas_connector.create_model, model_info) + mock_get_connection_info.assert_called_once_with(vim_id) + mock_add_model.assert_not_called() diff --git a/osm_lcm/tests/test_ns_activities.py b/osm_lcm/tests/test_ns_activities.py new file mode 100644 index 0000000..b12693b --- /dev/null +++ b/osm_lcm/tests/test_ns_activities.py @@ -0,0 +1,59 @@ +####################################################################################### +# 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. + + +import asynctest +from osm_common.dataclasses.temporal_dataclasses import ModelInfo, NsInstantiateInput +from osm_common.dbbase import DbException +from osm_lcm.temporal.ns_activities import NsDbActivity +from temporalio.testing import ActivityEnvironment +from unittest.mock import Mock + + +vim_id = "some-vim-uuid" +ns_id = "0123456789-9876543210" +nsr = { + "_id": ns_id, + "datacenter": vim_id, +} +input = NsInstantiateInput(ns_id, "op_id") +expected_namespace = "9-9876543210-ome-vim-uuid" +expected_model_info = ModelInfo(vim_id, expected_namespace) + + +class TestGetModelInfo(asynctest.TestCase): + def setUp(self): + self.db = Mock() + self.env = ActivityEnvironment() + self.ns_db_activity = NsDbActivity(self.db) + + async def test_nominal_case(self): + self.db.get_one.return_value = nsr + model_info = await self.env.run(self.ns_db_activity.get_model_info, input) + self.assertEqual(model_info, expected_model_info) + + async def test_db_raises_exception(self): + self.db.get_one.side_effect = DbException("not found") + with self.assertRaises(DbException): + model_info = await self.env.run(self.ns_db_activity.get_model_info, input) + self.assertIsNone(model_info) + + async def test_no_datacenter_raises_exception(self): + nsr = {"_id": ns_id} + self.db.get_one.return_value = nsr + with self.assertRaises(TypeError): + model_info = await self.env.run(self.ns_db_activity.get_model_info, input) + self.assertIsNone(model_info)