From: Mark Beierl Date: Wed, 22 Mar 2023 14:17:36 +0000 (-0400) Subject: Initial commit X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F72%2F13072%2F3;hp=eb15daa81c163ca6cf69d4e59ad06ba3895a7c88;p=osm%2FN2VC.git Initial commit Change-Id: I508b55f7105bb20dc4dd8c25881524ff2ddd49a6 Signed-off-by: Mark Beierl --- diff --git a/n2vc/temporal_libjuju.py b/n2vc/temporal_libjuju.py new file mode 100644 index 0000000..a4b7752 --- /dev/null +++ b/n2vc/temporal_libjuju.py @@ -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)