X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FN2VC.git;a=blobdiff_plain;f=n2vc%2Flibjuju.py;h=64276d6df19b8d511dc7cb6e4e76ab0bca270e7d;hp=ce1c9df9f5287cce8801832ee745af54ac8b8406;hb=eacf5a7724815fb33802ceb0af246dfc959eb021;hpb=82b591ceed704c798ead2d9104085a08e75b511b diff --git a/n2vc/libjuju.py b/n2vc/libjuju.py index ce1c9df..64276d6 100644 --- a/n2vc/libjuju.py +++ b/n2vc/libjuju.py @@ -18,7 +18,7 @@ import typing import time -from juju.errors import JujuAPIError +import juju.errors from juju.model import Model from juju.machine import Machine from juju.application import Application @@ -174,7 +174,7 @@ class Libjuju: cloud_name=cloud.name, credential_name=cloud.credential_name, ) - except JujuAPIError as e: + except juju.errors.JujuAPIError as e: if "already exists" in e.message: pass else: @@ -345,7 +345,7 @@ class Libjuju: db_dict: dict = None, progress_timeout: float = None, total_timeout: float = None, - series: str = "xenial", + series: str = "bionic", wait: bool = True, ) -> (Machine, bool): """ @@ -566,6 +566,146 @@ class Libjuju: await self.disconnect_model(model) await self.disconnect_controller(controller) + async def add_unit( + self, + application_name: str, + model_name: str, + machine_id: str, + db_dict: dict = None, + progress_timeout: float = None, + total_timeout: float = None, + ): + """Add unit + + :param: application_name: Application name + :param: model_name: Model name + :param: machine_id Machine id + :param: db_dict: Dictionary with data of the DB to write the updates + :param: progress_timeout: Maximum time between two updates in the model + :param: total_timeout: Timeout for the entity to be active + + :return: None + """ + + model = None + controller = await self.get_controller() + try: + model = await self.get_model(controller, model_name) + application = self._get_application(model, application_name) + + if application is not None: + + # Checks if the given machine id in the model, + # otherwise function raises an error + _machine, _series = self._get_machine_info(model, machine_id) + + self.log.debug( + "Adding unit (machine {}) to application {} in model ~{}".format( + machine_id, application_name, model_name + ) + ) + + await application.add_unit(to=machine_id) + + await JujuModelWatcher.wait_for( + model=model, + entity=application, + progress_timeout=progress_timeout, + total_timeout=total_timeout, + db_dict=db_dict, + n2vc=self.n2vc, + vca_id=self.vca_connection._vca_id, + ) + self.log.debug( + "Unit is added to application {} in model {}".format( + application_name, model_name + ) + ) + else: + raise JujuApplicationNotFound( + "Application {} not exists".format(application_name) + ) + finally: + if model: + await self.disconnect_model(model) + await self.disconnect_controller(controller) + + async def destroy_unit( + self, + application_name: str, + model_name: str, + machine_id: str, + total_timeout: float = None, + ): + """Destroy unit + + :param: application_name: Application name + :param: model_name: Model name + :param: machine_id Machine id + :param: total_timeout: Timeout for the entity to be active + + :return: None + """ + + model = None + controller = await self.get_controller() + try: + model = await self.get_model(controller, model_name) + application = self._get_application(model, application_name) + + if application is None: + raise JujuApplicationNotFound( + "Application not found: {} (model={})".format( + application_name, model_name + ) + ) + + unit = self._get_unit(application, machine_id) + if not unit: + raise JujuError( + "A unit with machine id {} not in available units".format( + machine_id + ) + ) + + unit_name = unit.name + + self.log.debug( + "Destroying unit {} from application {} in model {}".format( + unit_name, application_name, model_name + ) + ) + await application.destroy_unit(unit_name) + + self.log.debug( + "Waiting for unit {} to be destroyed in application {} (model={})...".format( + unit_name, application_name, model_name + ) + ) + + # TODO: Add functionality in the Juju watcher to replace this kind of blocks + if total_timeout is None: + total_timeout = 3600 + end = time.time() + total_timeout + while time.time() < end: + if not self._get_unit(application, machine_id): + self.log.debug( + "The unit {} was destroyed in application {} (model={}) ".format( + unit_name, application_name, model_name + ) + ) + return + await asyncio.sleep(5) + self.log.debug( + "Unit {} is destroyed from application {} in model {}".format( + unit_name, application_name, model_name + ) + ) + finally: + if model: + await self.disconnect_model(model) + await self.disconnect_controller(controller) + async def deploy_charm( self, application_name: str, @@ -608,16 +748,10 @@ class Libjuju: model = await self.get_model(controller, model_name) try: - application = None if application_name not in model.applications: if machine_id is not None: - if machine_id not in model.machines: - msg = "Machine {} not found in model".format(machine_id) - self.log.error(msg=msg) - raise JujuMachineNotFound(msg) - machine = model.machines[machine_id] - series = machine.series + machine, series = self._get_machine_info(model, machine_id) application = await model.deploy( entity_url=path, @@ -751,15 +885,50 @@ class Libjuju: if model.applications and application_name in model.applications: return model.applications[application_name] + def _get_unit(self, application: Application, machine_id: str) -> Unit: + """Get unit + + :param: application: Application object + :param: machine_id: Machine id + + :return: Unit + """ + unit = None + for u in application.units: + if u.machine_id == machine_id: + unit = u + break + return unit + + def _get_machine_info( + self, + model, + machine_id: str, + ) -> (str, str): + """Get machine info + + :param: model: Model object + :param: machine_id: Machine id + + :return: (str, str): (machine, series) + """ + if machine_id not in model.machines: + msg = "Machine {} not found in model".format(machine_id) + self.log.error(msg=msg) + raise JujuMachineNotFound(msg) + machine = model.machines[machine_id] + return machine, machine.series + async def execute_action( self, application_name: str, model_name: str, action_name: str, db_dict: dict = None, + machine_id: str = None, progress_timeout: float = None, total_timeout: float = None, - **kwargs + **kwargs, ): """Execute action @@ -767,6 +936,7 @@ class Libjuju: :param: model_name: Model name :param: action_name: Name of the action :param: db_dict: Dictionary with data of the DB to write the updates + :param: machine_id Machine id :param: progress_timeout: Maximum time between two updates in the model :param: total_timeout: Timeout for the entity to be active @@ -789,14 +959,31 @@ class Libjuju: ) if application is None: raise JujuApplicationNotFound("Cannot execute action") - - # Get leader unit # Racing condition: # Ocassionally, self._get_leader_unit() will return None # because the leader elected hook has not been triggered yet. # Therefore, we are doing some retries. If it happens again, # re-open bug 1236 - unit = await self._get_leader_unit(application) + if machine_id is None: + unit = await self._get_leader_unit(application) + self.log.debug( + "Action {} is being executed on the leader unit {}".format( + action_name, unit.name + ) + ) + else: + unit = self._get_unit(application, machine_id) + if not unit: + raise JujuError( + "A unit with machine id {} not in available units".format( + machine_id + ) + ) + self.log.debug( + "Action {} is being executed on {} unit".format( + action_name, unit.name + ) + ) actions = await application.get_actions() @@ -921,7 +1108,7 @@ class Libjuju: # Add relation try: await model.add_relation(endpoint_1, endpoint_2) - except JujuAPIError as e: + except juju.errors.JujuAPIError as e: if "not found" in e.message: self.log.warning("Relation not found: {}".format(e.message)) return @@ -958,7 +1145,7 @@ class Libjuju: await self.disconnect_model(model) await self.disconnect_controller(controller) - async def destroy_model(self, model_name: str, total_timeout: float): + async def destroy_model(self, model_name: str, total_timeout: float = 1800): """ Destroy model @@ -972,42 +1159,50 @@ class Libjuju: if not await self.model_exists(model_name, controller=controller): return - model = await self.get_model(controller, model_name) self.log.debug("Destroying model {}".format(model_name)) - uuid = model.info.uuid + model = await self.get_model(controller, model_name) # Destroy machines that are manually provisioned # and still are in pending state await self._destroy_pending_machines(model, only_manual=True) - - # Disconnect model await self.disconnect_model(model) - await controller.destroy_model(uuid, force=True, max_wait=0) + await self._destroy_model( + model_name, + controller, + timeout=total_timeout, + ) + finally: + if model: + await self.disconnect_model(model) + await self.disconnect_controller(controller) + + async def _destroy_model( + self, model_name: str, controller: Controller, timeout: float = 1800 + ): + """ + Destroy model from controller - # Wait until model is destroyed - self.log.debug("Waiting for model {} to be destroyed...".format(model_name)) + :param: model: Model name to be removed + :param: controller: Controller object + :param: timeout: Timeout in seconds + """ - if total_timeout is None: - total_timeout = 3600 - end = time.time() + total_timeout - while time.time() < end: - models = await controller.list_models() - if model_name not in models: - self.log.debug( - "The model {} ({}) was destroyed".format(model_name, uuid) - ) - return + async def _destroy_model_loop(model_name: str, controller: Controller): + while await self.model_exists(model_name, controller=controller): + await controller.destroy_model( + model_name, destroy_storage=True, force=True, max_wait=0 + ) await asyncio.sleep(5) + + try: + await asyncio.wait_for( + _destroy_model_loop(model_name, controller), timeout=timeout + ) + except asyncio.TimeoutError: raise Exception( "Timeout waiting for model {} to be destroyed".format(model_name) ) - except Exception as e: - if model: - await self.disconnect_model(model) - raise e - finally: - await self.disconnect_controller(controller) async def destroy_application( self, model_name: str, application_name: str, total_timeout: float @@ -1294,6 +1489,11 @@ class Libjuju: controller = await self.get_controller() try: await controller.remove_cloud(name) + except juju.errors.JujuError as e: + if len(e.errors) == 1 and f'cloud "{name}" not found' == e.errors[0]: + self.log.warning(f"Cloud {name} not found, so it could not be deleted.") + else: + raise e finally: await self.disconnect_controller(controller) @@ -1327,3 +1527,27 @@ class Libjuju: return (await facade.Credential(params)).results finally: await self.disconnect_controller(controller) + + async def check_application_exists(self, model_name, application_name) -> bool: + """Check application exists + + :param: model_name: Model Name + :param: application_name: Application Name + + :return: Boolean + """ + + model = None + controller = await self.get_controller() + try: + model = await self.get_model(controller, model_name) + self.log.debug( + "Checking if application {} exists in model {}".format( + application_name, model_name + ) + ) + return self._get_application(model, application_name) is not None + finally: + if model: + await self.disconnect_model(model) + await self.disconnect_controller(controller)