--- /dev/null
+# 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)