Initial commit 72/13072/3
authorMark Beierl <mark.beierl@canonical.com>
Wed, 22 Mar 2023 14:17:36 +0000 (10:17 -0400)
committerMark Beierl <mark.beierl@canonical.com>
Thu, 23 Mar 2023 10:08:02 +0000 (10:08 +0000)
Change-Id: I508b55f7105bb20dc4dd8c25881524ff2ddd49a6
Signed-off-by: Mark Beierl <mark.beierl@canonical.com>
n2vc/temporal_libjuju.py [new file with mode: 0644]

diff --git a/n2vc/temporal_libjuju.py b/n2vc/temporal_libjuju.py
new file mode 100644 (file)
index 0000000..a4b7752
--- /dev/null
@@ -0,0 +1,220 @@
+# Copyright 2023 Canonical Ltd.
+# 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 logging
+from dataclasses import dataclass
+from typing import List
+
+from juju.controller import Controller
+from juju.errors import JujuAPIError, JujuError
+from juju.model import Model
+
+from n2vc.exceptions import JujuApplicationExists, JujuControllerFailedConnecting
+
+
+@dataclass
+class ConnectionInfo:
+    """Information to connect to juju controller"""
+
+    endpoint: str
+    user: str
+    password: str
+    cacert: str
+    cloud_name: str
+    cloud_credentials: str
+
+    def __repr__(self):
+        return f"{self.__class__.__name__}(endpoint: {self.endpoint}, user: {self.user}, password: ******, caert: ******)"
+
+    def __str__(self):
+        return f"{self.__class__.__name__}(endpoint: {self.endpoint}, user: {self.user}, password: ******, caert: ******)"
+
+
+class Libjuju:
+    def __init__(self, connection_info: ConnectionInfo) -> None:
+        self.logger = logging.getLogger("temporal_libjuju")
+        self.connection_info = connection_info
+
+    async def get_controller(self) -> Controller:
+        controller = Controller()
+        try:
+            await controller.connect(
+                endpoint=self.connection_info.endpoint,
+                username=self.connection_info.user,
+                password=self.connection_info.password,
+                cacert=self.connection_info.cacert,
+            )
+            return controller
+        except Exception as e:
+            self.logger.error(
+                "Error connecting to controller={}: {}".format(
+                    self.connection_info.endpoint, e
+                )
+            )
+            await self.disconnect_controller(controller)
+            raise JujuControllerFailedConnecting(str(e))
+
+    async def disconnect_controller(self, controller: Controller) -> None:
+        if controller:
+            await controller.disconnect()
+
+    async def disconnect_model(self, model: Model):
+        if model:
+            await model.disconnect()
+
+    async def add_model(self, model_name: str):
+        model = None
+        controller = None
+        try:
+            controller = await self.get_controller()
+            if await self.model_exists(model_name, controller=controller):
+                return
+            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)
+
+    async def model_exists(
+        self, model_name: str, controller: Controller = None
+    ) -> bool:
+        """Returns True if model exists. False otherwhise."""
+        need_to_disconnect = False
+        try:
+            if not controller:
+                controller = await self.get_controller()
+                need_to_disconnect = True
+
+            return model_name in await controller.list_models()
+        finally:
+            if need_to_disconnect:
+                await self.disconnect_controller(controller)
+
+    async def get_model(self, model_name: str, controller: Controller) -> Model:
+        return await controller.get_model(model_name)
+
+    async def list_models(self) -> List[str]:
+        """List models in controller."""
+        try:
+            controller = await self.get_controller()
+            return await controller.list_models()
+        finally:
+            await self.disconnect_controller(controller)
+
+    async def deploy_charm(
+        self,
+        application_name: str,
+        path: str,
+        model_name: str,
+        config: dict = None,
+        series: str = None,
+        num_units: int = 1,
+        channel: str = "stable",
+    ):
+        """
+        Args:
+            application_name (str): Application name.
+            path (str): Local path to the charm.
+            model_name (str): Model name.
+            config (dict): Config for the charm.
+            series (str): Series of the charm.
+            num_units (str): Number of units to deploy.
+            channel (str): Charm store channel from which to retrieve the charm.
+
+        Returns:
+            (juju.application.Application): Juju application
+        """
+        self.logger.debug(
+            "Deploying charm {} in model {}".format(application_name, model_name)
+        )
+        self.logger.debug("charm: {}".format(path))
+        controller = None
+        model = None
+        try:
+            controller = await self.get_controller()
+            model = await self.get_model(controller, model_name)
+            if application_name in model.applications:
+                raise JujuApplicationExists(
+                    "Application {} exists".format(application_name)
+                )
+            application = await model.deploy(
+                entity_url=path,
+                application_name=application_name,
+                channel=channel,
+                num_units=num_units,
+                series=series,
+                config=config,
+            )
+
+            self.logger.debug(
+                "Wait until application {} is ready in model {}".format(
+                    application_name, model_name
+                )
+            )
+            await self.wait_app_deployment_completion(application_name, model_name)
+
+        except JujuError as e:
+            if "already exists" in e.message:
+                raise JujuApplicationExists(
+                    "Application {} exists".format(application_name)
+                )
+            else:
+                raise e
+        finally:
+            await self.disconnect_model(model)
+            await self.disconnect_controller(controller)
+
+        return application
+
+    async def wait_app_deployment_completion(
+        self, application_name: str, model_name: str
+    ) -> None:
+        self.logger.debug(
+            "Application {} is ready in model {}".format(application_name, model_name)
+        )
+
+    async def destroy_model(self, model_name: str, force=False) -> None:
+        controller = None
+        model = None
+
+        try:
+            controller = await self.get_controller()
+            if not await self.model_exists(model_name, controller=controller):
+                self.logger.warn(f"Model {model_name} doesn't exist")
+                return
+
+            self.logger.debug(f"Getting model {model_name} to destroy")
+            model = await self.get_model(controller, model_name)
+            await self.disconnect_model(model)
+
+            await controller.destroy_model(
+                model_name, destroy_storage=True, force=force, max_wait=60
+            )
+
+        except Exception as e:
+            self.logger.warn(f"Failed deleting model {model_name}: {e}")
+            raise e
+        finally:
+            await self.disconnect_model(model)
+            await self.disconnect_controller(controller)