From d662528a895d15e718f3a0c3b4a60ecce0d336c6 Mon Sep 17 00:00:00 2001 From: Patricia Reinoso Date: Wed, 12 Apr 2023 15:58:50 +0000 Subject: [PATCH] OSMENG-992 - Implement create model activity An exception is raised if model already exists Change-Id: Id76f5f8162a1c051a8c03815a3ebedc9f5a73b4e Signed-off-by: Patricia Reinoso --- n2vc/temporal_libjuju.py | 19 +- n2vc/tests/unit/test_temporal_libjuju.py | 368 +++++++++++++++++++++++ 2 files changed, 378 insertions(+), 9 deletions(-) create mode 100644 n2vc/tests/unit/test_temporal_libjuju.py diff --git a/n2vc/temporal_libjuju.py b/n2vc/temporal_libjuju.py index c13d244..a043ba0 100644 --- a/n2vc/temporal_libjuju.py +++ b/n2vc/temporal_libjuju.py @@ -17,10 +17,14 @@ from dataclasses import dataclass from typing import List from juju.controller import Controller -from juju.errors import JujuAPIError, JujuError +from juju.errors import JujuError from juju.model import Model -from n2vc.exceptions import JujuApplicationExists, JujuControllerFailedConnecting +from n2vc.exceptions import ( + JujuApplicationExists, + JujuControllerFailedConnecting, + JujuModelAlreadyExists, +) @dataclass @@ -74,24 +78,21 @@ class Libjuju: await model.disconnect() async def add_model(self, model_name: str): + """Exception is raised if model_name already exists""" model = None controller = None try: controller = await self.get_controller() if await self.model_exists(model_name, controller=controller): - return + raise JujuModelAlreadyExists( + "Cannot create model {}".format(model_name) + ) self.logger.debug("Creating model {}".format(model_name)) model = await controller.add_model( model_name, - # config=self.vca_connection.data.model_config, cloud_name=self.connection_info.cloud_name, credential_name=self.connection_info.cloud_credentials, ) - except JujuAPIError as e: - if "already exists" in e.message: - pass - else: - raise e finally: await self.disconnect_model(model) await self.disconnect_controller(controller) diff --git a/n2vc/tests/unit/test_temporal_libjuju.py b/n2vc/tests/unit/test_temporal_libjuju.py new file mode 100644 index 0000000..9e1ff43 --- /dev/null +++ b/n2vc/tests/unit/test_temporal_libjuju.py @@ -0,0 +1,368 @@ +####################################################################################### +# 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 asyncio +import asynctest +import logging +import juju +from unittest.mock import Mock + +from n2vc.temporal_libjuju import Libjuju, ConnectionInfo +from n2vc.exceptions import ( + JujuApplicationExists, + JujuControllerFailedConnecting, + JujuModelAlreadyExists, +) + +cacert = """-----BEGIN CERTIFICATE----- +SOMECERT +-----END CERTIFICATE-----""" + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Controller") +class LibjujuTestCase(asynctest.TestCase): + def setUp( + self, + mock_base64_to_cacert=None, + ): + self.loop = asyncio.get_event_loop() + self.connection_info = ConnectionInfo( + "1.2.3.4:17070", "user", "secret", cacert, "k8s_cloud", "k8s_credentials" + ) + logging.disable(logging.CRITICAL) + self.libjuju = Libjuju(self.connection_info) + + +@asynctest.mock.patch("juju.controller.Controller.connect") +class GetControllerTest(LibjujuTestCase): + def setUp(self): + super(GetControllerTest, self).setUp() + + def test_get_controller(self, mock_connect): + controller = self.loop.run_until_complete(self.libjuju.get_controller()) + self.assertIsInstance(controller, juju.controller.Controller) + mock_connect.assert_called_with( + endpoint=self.connection_info.endpoint, + username=self.connection_info.user, + password=self.connection_info.password, + cacert=self.connection_info.cacert, + ) + + @asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") + def test_exception( + self, + mock_disconnect_controller, + mock_connect, + ): + mock_connect.side_effect = Exception() + controller = None + with self.assertRaises(JujuControllerFailedConnecting): + controller = self.loop.run_until_complete(self.libjuju.get_controller()) + self.assertIsNone(controller) + mock_disconnect_controller.assert_called() + + +class DisconnectTest(LibjujuTestCase): + def setUp(self): + super(DisconnectTest, self).setUp() + + @asynctest.mock.patch("juju.model.Model.disconnect") + def test_disconnect_model(self, mock_disconnect): + self.loop.run_until_complete(self.libjuju.disconnect_model(juju.model.Model())) + mock_disconnect.assert_called_once() + + @asynctest.mock.patch("juju.controller.Controller.disconnect") + def test_disconnect_controller(self, mock_disconnect): + self.loop.run_until_complete( + self.libjuju.disconnect_controller(juju.controller.Controller()) + ) + mock_disconnect.assert_called_once() + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.model_exists") +@asynctest.mock.patch("juju.controller.Controller.add_model") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_model") +class AddModelTest(LibjujuTestCase): + def setUp(self): + super(AddModelTest, self).setUp() + + def test_existing_model_raises_exception( + self, + mock_disconnect_model, + mock_disconnect_controller, + mock_add_model, + mock_model_exists, + mock_get_controller, + ): + mock_model_exists.return_value = True + mock_get_controller.return_value = juju.controller.Controller() + with self.assertRaises(JujuModelAlreadyExists): + self.loop.run_until_complete(self.libjuju.add_model("existing_model")) + mock_get_controller.assert_called() + mock_add_model.assert_not_called() + mock_disconnect_controller.assert_called() + + def test_non_existing_model( + self, + mock_disconnect_model, + mock_disconnect_controller, + mock_add_model, + mock_model_exists, + mock_get_controller, + ): + mock_model_exists.return_value = False + mock_get_controller.return_value = juju.controller.Controller() + new_model_name = "nonexisting_model" + self.loop.run_until_complete(self.libjuju.add_model(new_model_name)) + mock_add_model.assert_called_once_with( + new_model_name, + cloud_name=self.connection_info.cloud_name, + credential_name=self.connection_info.cloud_credentials, + ) + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_controller") +@asynctest.mock.patch("juju.controller.Controller.list_models") +class ModelExistsTest(LibjujuTestCase): + def setUp(self): + super(ModelExistsTest, self).setUp() + + async def test_existing_model( + self, + mock_list_models, + mock_get_controller, + ): + model_name = "existing_model" + mock_list_models.return_value = [model_name] + self.assertTrue( + await self.libjuju.model_exists(model_name, juju.controller.Controller()) + ) + + @asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") + async def test_no_controller( + self, + mock_disconnect_controller, + mock_list_models, + mock_get_controller, + ): + model_name = "existing_model" + mock_list_models.return_value = [model_name] + mock_get_controller.return_value = juju.controller.Controller() + self.assertTrue(await self.libjuju.model_exists(model_name)) + mock_disconnect_controller.assert_called_once() + + async def test_non_existing_model( + self, + mock_list_models, + mock_get_controller, + ): + mock_list_models.return_value = [] + self.assertFalse( + await self.libjuju.model_exists( + "not_existing_model", juju.controller.Controller() + ) + ) + + +@asynctest.mock.patch("juju.controller.Controller.get_model") +class GetModelTest(LibjujuTestCase): + def setUp(self): + super(GetModelTest, self).setUp() + + def test_get_model( + self, + mock_get_model, + ): + mock_get_model.return_value = juju.model.Model() + model = self.loop.run_until_complete( + self.libjuju.get_model(juju.controller.Controller(), "model") + ) + self.assertIsInstance(model, juju.model.Model) + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") +@asynctest.mock.patch("juju.controller.Controller.list_models") +class ListModelsTest(LibjujuTestCase): + def setUp(self): + super(ListModelsTest, self).setUp() + + def test_containing( + self, + mock_list_models, + mock_disconnect_controller, + mock_get_controller, + ): + expected_list = ["existingmodel"] + mock_get_controller.return_value = juju.controller.Controller() + mock_list_models.return_value = expected_list + models = self.loop.run_until_complete(self.libjuju.list_models()) + + mock_disconnect_controller.assert_called_once() + self.assertEquals(models, expected_list) + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_model") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_model") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.wait_app_deployment_completion") +@asynctest.mock.patch( + "juju.model.Model.applications", new_callable=asynctest.PropertyMock +) +@asynctest.mock.patch("juju.model.Model.deploy") +class DeployCharmTest(LibjujuTestCase): + def setUp(self): + super(DeployCharmTest, self).setUp() + + def test_existing_app_is_not_deployed( + self, + mock_deploy, + mock_applications, + mock_wait_app_deployment, + mock_disconnect_controller, + mock_disconnect_model, + mock_get_model, + mock_get_controller, + ): + mock_get_model.return_value = juju.model.Model() + mock_applications.return_value = {"existing_app"} + + application = None + with self.assertRaises(JujuApplicationExists): + application = self.loop.run_until_complete( + self.libjuju.deploy_charm( + "existing_app", + "path", + "model", + "machine", + ) + ) + self.assertIsNone(application) + + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() + + def test_app_is_deployed( + self, + mock_deploy, + mock_applications, + mock_wait_app_deployment, + mock_disconnect_controller, + mock_disconnect_model, + mock_get_model, + mock_get_controller, + ): + mock_get_model.return_value = juju.model.Model() + mock_deploy.return_value = Mock() + self.loop.run_until_complete( + self.libjuju.deploy_charm( + "app", + "path", + "model", + series="series", + num_units=2, + ) + ) + mock_deploy.assert_called_once_with( + entity_url="path", + application_name="app", + channel="stable", + num_units=2, + series="series", + config=None, + ) + mock_wait_app_deployment.assert_called() + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() + + +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_controller") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.get_model") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.model_exists") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_model") +@asynctest.mock.patch("n2vc.temporal_libjuju.Libjuju.disconnect_controller") +@asynctest.mock.patch("juju.controller.Controller.destroy_model") +class DestroyModelTest(LibjujuTestCase): + def setUp(self): + super(DestroyModelTest, self).setUp() + + def test_model_is_destroyed( + self, + mock_destroy, + mock_disconnect_controller, + mock_disconnect_model, + mock_model_exists, + mock_get_model, + mock_get_controller, + ): + mock_get_controller.return_value = juju.controller.Controller() + mock_model_exists.return_value = True + mock_get_model.return_value = juju.model.Model() + model_name = "model_to_destroy" + force = True + + self.loop.run_until_complete(self.libjuju.destroy_model(model_name, force)) + mock_destroy.assert_called_with( + model_name, destroy_storage=True, force=force, max_wait=60 + ) + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() + + def test_not_existing_model( + self, + mock_destroy, + mock_disconnect_controller, + mock_disconnect_model, + mock_model_exists, + mock_get_model, + mock_get_controller, + ): + mock_get_controller.return_value = juju.controller.Controller() + mock_model_exists.return_value = False + model_name = "model_to_destroy" + force = True + + self.loop.run_until_complete(self.libjuju.destroy_model(model_name, force)) + mock_destroy.assert_not_called() + mock_get_model.assert_not_called() + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() + + def test_raise_exception( + self, + mock_destroy, + mock_disconnect_controller, + mock_disconnect_model, + mock_model_exists, + mock_get_model, + mock_get_controller, + ): + mock_get_controller.return_value = juju.controller.Controller() + mock_model_exists.return_value = True + mock_get_model.side_effect = Exception() + model_name = "model_to_destroy" + force = False + + with self.assertRaises(Exception): + self.loop.run_until_complete(self.libjuju.destroy_model(model_name, force)) + mock_destroy.assert_not_called() + mock_disconnect_controller.assert_called() + mock_disconnect_model.assert_called() -- 2.25.1