+ model,
+ application_name=application_name,
+ )
+ if application is None:
+ raise JujuApplicationNotFound(
+ "Cannot find application {} to resolve".format(application_name)
+ )
+
+ while application.status == "error":
+ for unit in application.units:
+ if unit.workload_status == "error":
+ self.log.debug(
+ "Model {}, Application {}, Unit {} in error state, resolving".format(
+ model_name, application_name, unit.entity_id
+ )
+ )
+ try:
+ await unit.resolved(retry=False)
+ except Exception:
+ pass
+
+ await asyncio.sleep(1)
+
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ async def resolve(self, model_name: str):
+
+ controller = await self.get_controller()
+ model = await self.get_model(controller, model_name)
+ all_units_active = False
+ try:
+ while not all_units_active:
+ all_units_active = True
+ for application_name, application in model.applications.items():
+ if application.status == "error":
+ for unit in application.units:
+ if unit.workload_status == "error":
+ self.log.debug(
+ "Model {}, Application {}, Unit {} in error state, resolving".format(
+ model_name, application_name, unit.entity_id
+ )
+ )
+ try:
+ await unit.resolved(retry=False)
+ all_units_active = False
+ except Exception:
+ pass
+
+ if not all_units_active:
+ await asyncio.sleep(5)
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ async def scale_application(
+ self,
+ model_name: str,
+ application_name: str,
+ scale: int = 1,
+ total_timeout: float = None,
+ ):
+ """
+ Scale application (K8s)
+
+ :param: model_name: Model name
+ :param: application_name: Application name
+ :param: scale: Scale to which to set this application
+ :param: total_timeout: Timeout for the entity to be active
+ """
+
+ model = None
+ controller = await self.get_controller()
+ try:
+ model = await self.get_model(controller, model_name)
+
+ self.log.debug(
+ "Scaling application {} in model {}".format(
+ application_name, model_name
+ )
+ )
+ application = self._get_application(model, application_name)
+ if application is None:
+ raise JujuApplicationNotFound("Cannot scale application")
+ await application.scale(scale=scale)
+ # Wait until application is scaled in model
+ self.log.debug(
+ "Waiting for application {} to be scaled in model {}...".format(
+ application_name, model_name
+ )
+ )
+ if total_timeout is None:
+ total_timeout = 1800
+ end = time.time() + total_timeout
+ while time.time() < end:
+ application_scale = self._get_application_count(model, application_name)
+ # Before calling wait_for_model function,
+ # wait until application unit count and scale count are equal.
+ # Because there is a delay before scaling triggers in Juju model.
+ if application_scale == scale:
+ await JujuModelWatcher.wait_for_model(
+ model=model, timeout=total_timeout
+ )
+ self.log.debug(
+ "Application {} is scaled in model {}".format(
+ application_name, model_name
+ )
+ )
+ return
+ await asyncio.sleep(5)
+ raise Exception(
+ "Timeout waiting for application {} in model {} to be scaled".format(
+ application_name, model_name
+ )
+ )
+ finally:
+ if model:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ def _get_application_count(self, model: Model, application_name: str) -> int:
+ """Get number of units of the application
+
+ :param: model: Model object
+ :param: application_name: Application name
+
+ :return: int (or None if application doesn't exist)
+ """
+ application = self._get_application(model, application_name)
+ if application is not None:
+ return len(application.units)
+
+ def _get_application(self, model: Model, application_name: str) -> Application:
+ """Get application
+
+ :param: model: Model object
+ :param: application_name: Application name
+
+ :return: juju.application.Application (or None if it doesn't exist)
+ """
+ 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,
+ ):
+ """Execute action
+
+ :param: application_name: Application name
+ :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
+
+ :return: (str, str): (output and status)
+ """
+ self.log.debug(
+ "Executing action {} using params {}".format(action_name, kwargs)
+ )
+ # Get controller
+ controller = await self.get_controller()
+
+ # Get model
+ model = await self.get_model(controller, model_name)
+
+ try:
+ # Get application
+ application = self._get_application(
+ model,
+ application_name=application_name,
+ )
+ if application is None:
+ raise JujuApplicationNotFound("Cannot execute action")
+ # 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
+ 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()
+
+ if action_name not in actions:
+ raise JujuActionNotFound(
+ "Action {} not in available actions".format(action_name)
+ )
+
+ action = await unit.run_action(action_name, **kwargs)
+
+ self.log.debug(
+ "Wait until action {} is completed in application {} (model={})".format(
+ action_name, application_name, model_name
+ )
+ )
+ await JujuModelWatcher.wait_for(
+ model=model,
+ entity=action,
+ progress_timeout=progress_timeout,
+ total_timeout=total_timeout,
+ db_dict=db_dict,
+ n2vc=self.n2vc,
+ vca_id=self.vca_connection._vca_id,
+ )
+
+ output = await model.get_action_output(action_uuid=action.entity_id)
+ status = await model.get_action_status(uuid_or_prefix=action.entity_id)
+ status = (
+ status[action.entity_id] if action.entity_id in status else "failed"
+ )
+
+ self.log.debug(
+ "Action {} completed with status {} in application {} (model={})".format(
+ action_name, action.status, application_name, model_name
+ )
+ )
+ finally:
+ await self.disconnect_model(model)
+ await self.disconnect_controller(controller)
+
+ return output, status
+
+ async def get_actions(self, application_name: str, model_name: str) -> dict:
+ """Get list of actions
+
+ :param: application_name: Application name
+ :param: model_name: Model name
+
+ :return: Dict with this format
+ {
+ "action_name": "Description of the action",
+ ...
+ }
+ """
+ self.log.debug(
+ "Getting list of actions for application {}".format(application_name)
+ )
+
+ # Get controller
+ controller = await self.get_controller()
+
+ # Get model
+ model = await self.get_model(controller, model_name)
+
+ try:
+ # Get application
+ application = self._get_application(
+ model,
+ application_name=application_name,