OSMENG-992 - Implement create model activity 92/13192/5
authorPatricia Reinoso <patricia.reinoso@canonical.com>
Wed, 12 Apr 2023 15:58:50 +0000 (15:58 +0000)
committerPatricia Reinoso <patricia.reinoso@canonical.com>
Thu, 13 Apr 2023 08:45:06 +0000 (08:45 +0000)
An exception is raised if model already exists

Change-Id: Id76f5f8162a1c051a8c03815a3ebedc9f5a73b4e
Signed-off-by: Patricia Reinoso <patricia.reinoso@canonical.com>
n2vc/temporal_libjuju.py
n2vc/tests/unit/test_temporal_libjuju.py [new file with mode: 0644]

index c13d244..a043ba0 100644 (file)
@@ -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 (file)
index 0000000..9e1ff43
--- /dev/null
@@ -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()