Pin black version in tox.ini to 23.12.1
[osm/N2VC.git] / n2vc / libjuju.py
index 7a73033..f36ff39 100644 (file)
 
 import asyncio
 import logging
 
 import asyncio
 import logging
+import os
+import typing
+import yaml
 
 import time
 
 
 import time
 
-from juju.errors import JujuAPIError
+import juju.errors
+from juju.bundle import BundleHandler
 from juju.model import Model
 from juju.machine import Machine
 from juju.application import Application
 from juju.unit import Unit
 from juju.model import Model
 from juju.machine import Machine
 from juju.application import Application
 from juju.unit import Unit
+from juju.url import URL
+from juju.version import DEFAULT_ARCHITECTURE
 from juju.client._definitions import (
     FullStatus,
     QueryApplicationOffersResults,
 from juju.client._definitions import (
     FullStatus,
     QueryApplicationOffersResults,
@@ -32,6 +38,7 @@ from juju.controller import Controller
 from juju.client import client
 from juju import tag
 
 from juju.client import client
 from juju import tag
 
+from n2vc.definitions import Offer, RelationEndpoint
 from n2vc.juju_watcher import JujuModelWatcher
 from n2vc.provisioner import AsyncSSHProvisioner
 from n2vc.n2vc_conn import N2VCConnector
 from n2vc.juju_watcher import JujuModelWatcher
 from n2vc.provisioner import AsyncSSHProvisioner
 from n2vc.n2vc_conn import N2VCConnector
@@ -43,79 +50,53 @@ from n2vc.exceptions import (
     JujuControllerFailedConnecting,
     JujuApplicationExists,
     JujuInvalidK8sConfiguration,
     JujuControllerFailedConnecting,
     JujuApplicationExists,
     JujuInvalidK8sConfiguration,
+    JujuError,
 )
 )
-from n2vc.utils import DB_DATA
-from osm_common.dbbase import DbException
+from n2vc.vca.cloud import Cloud as VcaCloud
+from n2vc.vca.connection import Connection
 from kubernetes.client.configuration import Configuration
 from kubernetes.client.configuration import Configuration
+from retrying_async import retry
+
 
 RBAC_LABEL_KEY_NAME = "rbac-id"
 
 
 
 RBAC_LABEL_KEY_NAME = "rbac-id"
 
 
+@asyncio.coroutine
+def retry_callback(attempt, exc, args, kwargs, delay=0.5, *, loop):
+    # Specifically overridden from upstream implementation so it can
+    # continue to work with Python 3.10
+    yield from asyncio.sleep(attempt * delay)
+    return retry
+
+
 class Libjuju:
     def __init__(
         self,
 class Libjuju:
     def __init__(
         self,
-        endpoint: str,
-        api_proxy: str,
-        username: str,
-        password: str,
-        cacert: str,
-        loop: asyncio.AbstractEventLoop = None,
+        vca_connection: Connection,
         log: logging.Logger = None,
         log: logging.Logger = None,
-        db: dict = None,
         n2vc: N2VCConnector = None,
         n2vc: N2VCConnector = None,
-        apt_mirror: str = None,
-        enable_os_upgrade: bool = True,
     ):
         """
         Constructor
 
     ):
         """
         Constructor
 
-        :param: endpoint:               Endpoint of the juju controller (host:port)
-        :param: api_proxy:              Endpoint of the juju controller - Reachable from the VNFs
-        :param: username:               Juju username
-        :param: password:               Juju password
-        :param: cacert:                 Juju CA Certificate
-        :param: loop:                   Asyncio loop
+        :param: vca_connection:         n2vc.vca.connection object
         :param: log:                    Logger
         :param: log:                    Logger
-        :param: db:                     DB object
         :param: n2vc:                   N2VC object
         :param: n2vc:                   N2VC object
-        :param: apt_mirror:             APT Mirror
-        :param: enable_os_upgrade:      Enable OS Upgrade
         """
 
         self.log = log or logging.getLogger("Libjuju")
         """
 
         self.log = log or logging.getLogger("Libjuju")
-        self.db = db
-        db_endpoints = self._get_api_endpoints_db()
-        self.endpoints = None
-        if (db_endpoints and endpoint not in db_endpoints) or not db_endpoints:
-            self.endpoints = [endpoint]
-            self._update_api_endpoints_db(self.endpoints)
-        else:
-            self.endpoints = db_endpoints
-        self.api_proxy = api_proxy
-        self.username = username
-        self.password = password
-        self.cacert = cacert
-        self.loop = loop or asyncio.get_event_loop()
         self.n2vc = n2vc
         self.n2vc = n2vc
+        self.vca_connection = vca_connection
 
 
-        # Generate config for models
-        self.model_config = {}
-        if apt_mirror:
-            self.model_config["apt-mirror"] = apt_mirror
-        self.model_config["enable-os-refresh-update"] = enable_os_upgrade
-        self.model_config["enable-os-upgrade"] = enable_os_upgrade
-
-        self.loop.set_exception_handler(self.handle_exception)
-        self.creating_model = asyncio.Lock(loop=self.loop)
+        self.creating_model = asyncio.Lock()
 
 
-        self.log.debug("Libjuju initialized!")
-
-        self.health_check_task = self._create_health_check_task()
+        if self.vca_connection.is_default:
+            self.health_check_task = self._create_health_check_task()
 
     def _create_health_check_task(self):
 
     def _create_health_check_task(self):
-        return self.loop.create_task(self.health_check())
+        return asyncio.get_event_loop().create_task(self.health_check())
 
 
-    async def get_controller(self, timeout: float = 15.0) -> Controller:
+    async def get_controller(self, timeout: float = 60.0) -> Controller:
         """
         Get controller
 
         """
         Get controller
 
@@ -123,30 +104,37 @@ class Libjuju:
         """
         controller = None
         try:
         """
         controller = None
         try:
-            controller = Controller(loop=self.loop)
+            controller = Controller()
             await asyncio.wait_for(
                 controller.connect(
             await asyncio.wait_for(
                 controller.connect(
-                    endpoint=self.endpoints,
-                    username=self.username,
-                    password=self.password,
-                    cacert=self.cacert,
+                    endpoint=self.vca_connection.data.endpoints,
+                    username=self.vca_connection.data.user,
+                    password=self.vca_connection.data.secret,
+                    cacert=self.vca_connection.data.cacert,
                 ),
                 timeout=timeout,
             )
                 ),
                 timeout=timeout,
             )
-            endpoints = await controller.api_endpoints
-            if self.endpoints != endpoints:
-                self.endpoints = endpoints
-                self._update_api_endpoints_db(self.endpoints)
+            if self.vca_connection.is_default:
+                endpoints = await controller.api_endpoints
+                if not all(
+                    endpoint in self.vca_connection.endpoints for endpoint in endpoints
+                ):
+                    await self.vca_connection.update_endpoints(endpoints)
             return controller
         except asyncio.CancelledError as e:
             raise e
         except Exception as e:
             self.log.error(
             return controller
         except asyncio.CancelledError as e:
             raise e
         except Exception as e:
             self.log.error(
-                "Failed connecting to controller: {}...".format(self.endpoints)
+                "Failed connecting to controller: {}... {}".format(
+                    self.vca_connection.data.endpoints, e
+                )
             )
             if controller:
                 await self.disconnect_controller(controller)
             )
             if controller:
                 await self.disconnect_controller(controller)
-            raise JujuControllerFailedConnecting(e)
+
+            raise JujuControllerFailedConnecting(
+                f"Error connecting to Juju controller: {e}"
+            )
 
     async def disconnect(self):
         """Disconnect"""
 
     async def disconnect(self):
         """Disconnect"""
@@ -171,14 +159,13 @@ class Libjuju:
         if controller:
             await controller.disconnect()
 
         if controller:
             await controller.disconnect()
 
-    async def add_model(self, model_name: str, cloud_name: str, credential_name=None):
+    @retry(attempts=3, delay=5, timeout=None, callback=retry_callback)
+    async def add_model(self, model_name: str, cloud: VcaCloud):
         """
         Create model
 
         :param: model_name: Model name
         """
         Create model
 
         :param: model_name: Model name
-        :param: cloud_name: Cloud name
-        :param: credential_name: Credential name to use for adding the model
-                                 If not specified, same name as the cloud will be used.
+        :param: cloud: Cloud object
         """
 
         # Get controller
         """
 
         # Get controller
@@ -196,18 +183,99 @@ class Libjuju:
                 self.log.debug("Creating model {}".format(model_name))
                 model = await controller.add_model(
                     model_name,
                 self.log.debug("Creating model {}".format(model_name))
                 model = await controller.add_model(
                     model_name,
-                    config=self.model_config,
-                    cloud_name=cloud_name,
-                    credential_name=credential_name or cloud_name,
+                    config=self.vca_connection.data.model_config,
+                    cloud_name=cloud.name,
+                    credential_name=cloud.credential_name,
+                )
+        except juju.errors.JujuAPIError as e:
+            if "already exists" in e.message:
+                pass
+            else:
+                raise e
+        finally:
+            if model:
+                await self.disconnect_model(model)
+            await self.disconnect_controller(controller)
+
+    async def get_executed_actions(self, model_name: str) -> list:
+        """
+        Get executed/history of actions for a model.
+
+        :param: model_name: Model name, str.
+        :return: List of executed actions for a model.
+        """
+        model = None
+        executed_actions = []
+        controller = await self.get_controller()
+        try:
+            model = await self.get_model(controller, model_name)
+            # Get all unique action names
+            actions = {}
+            for application in model.applications:
+                application_actions = await self.get_actions(application, model_name)
+                actions.update(application_actions)
+            # Get status of all actions
+            for application_action in actions:
+                app_action_status_list = await model.get_action_status(
+                    name=application_action
+                )
+                for action_id, action_status in app_action_status_list.items():
+                    executed_action = {
+                        "id": action_id,
+                        "action": application_action,
+                        "status": action_status,
+                    }
+                    # Get action output by id
+                    action_status = await model.get_action_output(executed_action["id"])
+                    for k, v in action_status.items():
+                        executed_action[k] = v
+                    executed_actions.append(executed_action)
+        except Exception as e:
+            raise JujuError(
+                "Error in getting executed actions for model: {}. Error: {}".format(
+                    model_name, str(e)
+                )
+            )
+        finally:
+            if model:
+                await self.disconnect_model(model)
+            await self.disconnect_controller(controller)
+        return executed_actions
+
+    async def get_application_configs(
+        self, model_name: str, application_name: str
+    ) -> dict:
+        """
+        Get available configs for an application.
+
+        :param: model_name: Model name, str.
+        :param: application_name: Application name, str.
+
+        :return: A dict which has key - action name, value - action description
+        """
+        model = None
+        application_configs = {}
+        controller = await self.get_controller()
+        try:
+            model = await self.get_model(controller, model_name)
+            application = self._get_application(
+                model, application_name=application_name
+            )
+            application_configs = await application.get_config()
+        except Exception as e:
+            raise JujuError(
+                "Error in getting configs for application: {} in model: {}. Error: {}".format(
+                    application_name, model_name, str(e)
                 )
                 )
+            )
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
+        return application_configs
 
 
-    async def get_model(
-        self, controller: Controller, model_name: str, id=None
-    ) -> Model:
+    @retry(attempts=3, delay=5, callback=retry_callback)
+    async def get_model(self, controller: Controller, model_name: str) -> Model:
         """
         Get model from controller
 
         """
         Get model from controller
 
@@ -290,7 +358,7 @@ class Libjuju:
         db_dict: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
         db_dict: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
-        series: str = "xenial",
+        series: str = "bionic",
         wait: bool = True,
     ) -> (Machine, bool):
         """
         wait: bool = True,
     ) -> (Machine, bool):
         """
@@ -362,6 +430,7 @@ class Libjuju:
                         total_timeout=total_timeout,
                         db_dict=db_dict,
                         n2vc=self.n2vc,
                         total_timeout=total_timeout,
                         db_dict=db_dict,
                         n2vc=self.n2vc,
+                        vca_id=self.vca_connection._vca_id,
                     )
         finally:
             await self.disconnect_model(model)
                     )
         finally:
             await self.disconnect_model(model)
@@ -443,7 +512,7 @@ class Libjuju:
                     connection=connection,
                     nonce=params.nonce,
                     machine_id=machine_id,
                     connection=connection,
                     nonce=params.nonce,
                     machine_id=machine_id,
-                    proxy=self.api_proxy,
+                    proxy=self.vca_connection.data.api_proxy,
                     series=params.series,
                 )
             )
                     series=params.series,
                 )
             )
@@ -474,6 +543,7 @@ class Libjuju:
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
+                vca_id=self.vca_connection._vca_id,
             )
         except Exception as e:
             raise e
             )
         except Exception as e:
             raise e
@@ -488,27 +558,261 @@ class Libjuju:
         return machine_id
 
     async def deploy(
         return machine_id
 
     async def deploy(
-        self, uri: str, model_name: str, wait: bool = True, timeout: float = 3600
+        self,
+        uri: str,
+        model_name: str,
+        wait: bool = True,
+        timeout: float = 3600,
+        instantiation_params: dict = None,
     ):
         """
         Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
 
     ):
         """
         Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
 
-        :param: uri:            Path or Charm Store uri in which the charm or bundle can be found
-        :param: model_name:     Model name
-        :param: wait:           Indicates whether to wait or not until all applications are active
-        :param: timeout:        Time in seconds to wait until all applications are active
+        :param uri:            Path or Charm Store uri in which the charm or bundle can be found
+        :param model_name:     Model name
+        :param wait:           Indicates whether to wait or not until all applications are active
+        :param timeout:        Time in seconds to wait until all applications are active
+        :param instantiation_params: To be applied as overlay bundle over primary bundle.
         """
         controller = await self.get_controller()
         model = await self.get_model(controller, model_name)
         """
         controller = await self.get_controller()
         model = await self.get_model(controller, model_name)
+        overlays = []
         try:
         try:
-            await model.deploy(uri)
+            await self._validate_instantiation_params(uri, model, instantiation_params)
+            overlays = self._get_overlays(model_name, instantiation_params)
+            await model.deploy(uri, trust=True, overlays=overlays)
             if wait:
                 await JujuModelWatcher.wait_for_model(model, timeout=timeout)
                 self.log.debug("All units active in model {}".format(model_name))
         finally:
             if wait:
                 await JujuModelWatcher.wait_for_model(model, timeout=timeout)
                 self.log.debug("All units active in model {}".format(model_name))
         finally:
+            self._remove_overlay_file(overlays)
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
+    async def _validate_instantiation_params(
+        self, uri: str, model, instantiation_params: dict
+    ) -> None:
+        """Checks if all the applications in instantiation_params
+        exist ins the original bundle.
+
+        Raises:
+            JujuApplicationNotFound if there is an invalid app in
+            the instantiation params.
+        """
+        overlay_apps = self._get_apps_in_instantiation_params(instantiation_params)
+        if not overlay_apps:
+            return
+        original_apps = await self._get_apps_in_original_bundle(uri, model)
+        if not all(app in original_apps for app in overlay_apps):
+            raise JujuApplicationNotFound(
+                "Cannot find application {} in original bundle {}".format(
+                    overlay_apps, original_apps
+                )
+            )
+
+    async def _get_apps_in_original_bundle(self, uri: str, model) -> set:
+        """Bundle is downloaded in BundleHandler.fetch_plan.
+        That method takes care of opening and exception handling.
+
+        Resolve method gets all the information regarding the channel,
+        track, revision, type, source.
+
+        Returns:
+            Set with the names of the applications in original bundle.
+        """
+        url = URL.parse(uri)
+        architecture = DEFAULT_ARCHITECTURE  # only AMD64 is allowed
+        res = await model.deploy_types[str(url.schema)].resolve(
+            url, architecture, entity_url=uri
+        )
+        handler = BundleHandler(model, trusted=True, forced=False)
+        await handler.fetch_plan(url, res.origin)
+        return handler.applications
+
+    def _get_apps_in_instantiation_params(self, instantiation_params: dict) -> list:
+        """Extract applications key in instantiation params.
+
+        Returns:
+            List with the names of the applications in instantiation params.
+
+        Raises:
+            JujuError if applications key is not found.
+        """
+        if not instantiation_params:
+            return []
+        try:
+            return [key for key in instantiation_params.get("applications")]
+        except Exception as e:
+            raise JujuError("Invalid overlay format. {}".format(str(e)))
+
+    def _get_overlays(self, model_name: str, instantiation_params: dict) -> list:
+        """Creates a temporary overlay file which includes the instantiation params.
+        Only one overlay file is created.
+
+        Returns:
+            List with one overlay filename. Empty list if there are no instantiation params.
+        """
+        if not instantiation_params:
+            return []
+        file_name = model_name + "-overlay.yaml"
+        self._write_overlay_file(file_name, instantiation_params)
+        return [file_name]
+
+    def _write_overlay_file(self, file_name: str, instantiation_params: dict) -> None:
+        with open(file_name, "w") as file:
+            yaml.dump(instantiation_params, file)
+
+    def _remove_overlay_file(self, overlay: list) -> None:
+        """Overlay contains either one or zero file names."""
+        if not overlay:
+            return
+        try:
+            filename = overlay[0]
+            os.remove(filename)
+        except OSError as e:
+            self.log.warning(
+                "Overlay file {} could not be removed: {}".format(filename, e)
+            )
+
+    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,
     async def deploy_charm(
         self,
         application_name: str,
@@ -551,16 +855,9 @@ class Libjuju:
         model = await self.get_model(controller, model_name)
 
         try:
         model = await self.get_model(controller, model_name)
 
         try:
-            application = None
             if application_name not in model.applications:
             if application_name not in model.applications:
-
                 if machine_id is not None:
                 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,
 
                 application = await model.deploy(
                     entity_url=path,
@@ -589,6 +886,7 @@ class Libjuju:
                     total_timeout=total_timeout,
                     db_dict=db_dict,
                     n2vc=self.n2vc,
                     total_timeout=total_timeout,
                     db_dict=db_dict,
                     n2vc=self.n2vc,
+                    vca_id=self.vca_connection._vca_id,
                 )
                 self.log.debug(
                     "Application {} is ready in model {}".format(
                 )
                 self.log.debug(
                     "Application {} is ready in model {}".format(
@@ -599,12 +897,241 @@ class Libjuju:
                 raise JujuApplicationExists(
                     "Application {} exists".format(application_name)
                 )
                 raise JujuApplicationExists(
                     "Application {} exists".format(application_name)
                 )
+        except juju.errors.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
 
         finally:
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
         return application
 
+    async def upgrade_charm(
+        self,
+        application_name: str,
+        path: str,
+        model_name: str,
+        total_timeout: float = None,
+        **kwargs,
+    ):
+        """Upgrade Charm
+
+        :param: application_name:   Application name
+        :param: model_name:         Model name
+        :param: path:               Local path to the charm
+        :param: total_timeout:      Timeout for the entity to be active
+
+        :return: (str, str): (output and status)
+        """
+
+        self.log.debug(
+            "Upgrading charm {} in model {} from path {}".format(
+                application_name, model_name, path
+            )
+        )
+
+        await self.resolve_application(
+            model_name=model_name, application_name=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,
+            )
+            if application is None:
+                raise JujuApplicationNotFound(
+                    "Cannot find application {} to upgrade".format(application_name)
+                )
+
+            await application.refresh(path=path)
+
+            self.log.debug(
+                "Wait until charm upgrade is completed for application {} (model={})".format(
+                    application_name, model_name
+                )
+            )
+
+            await JujuModelWatcher.ensure_units_idle(
+                model=model, application=application
+            )
+
+            if application.status == "error":
+                error_message = "Unknown"
+                for unit in application.units:
+                    if (
+                        unit.workload_status == "error"
+                        and unit.workload_status_message != ""
+                    ):
+                        error_message = unit.workload_status_message
+
+                message = "Application {} failed update in {}: {}".format(
+                    application_name, model_name, error_message
+                )
+                self.log.error(message)
+                raise JujuError(message=message)
+
+            self.log.debug(
+                "Application {} is ready in model {}".format(
+                    application_name, model_name
+                )
+            )
+
+        finally:
+            await self.disconnect_model(model)
+            await self.disconnect_controller(controller)
+
+        return application
+
+    async def resolve_application(self, model_name: str, application_name: str):
+        controller = await self.get_controller()
+        model = await self.get_model(controller, model_name)
+
+        try:
+            application = self._get_application(
+                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
 
     def _get_application(self, model: Model, application_name: str) -> Application:
         """Get application
 
@@ -616,15 +1143,50 @@ class Libjuju:
         if model.applications and application_name in model.applications:
             return model.applications[application_name]
 
         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,
     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,
         progress_timeout: float = None,
         total_timeout: float = None,
-        **kwargs
+        **kwargs,
     ):
         """Execute action
 
     ):
         """Execute action
 
@@ -632,6 +1194,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: 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
 
         :param: progress_timeout:   Maximum time between two updates in the model
         :param: total_timeout:      Timeout for the entity to be active
 
@@ -654,25 +1217,30 @@ class Libjuju:
             )
             if application is None:
                 raise JujuApplicationNotFound("Cannot execute action")
             )
             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
             # 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
-            attempts = 3
-            time_between_retries = 10
-            unit = None
-            for _ in range(attempts):
+            if machine_id is None:
                 unit = await self._get_leader_unit(application)
                 unit = await self._get_leader_unit(application)
-                if unit is None:
-                    await asyncio.sleep(time_between_retries)
-                else:
-                    break
-            if unit is None:
-                raise JujuLeaderUnitNotFound(
-                    "Cannot execute action: leader unit not found"
+                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()
                 )
 
             actions = await application.get_actions()
@@ -696,6 +1264,7 @@ class Libjuju:
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
                 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)
             )
 
             output = await model.get_action_output(action_uuid=action.entity_id)
@@ -797,11 +1366,11 @@ class Libjuju:
         # Add relation
         try:
             await model.add_relation(endpoint_1, endpoint_2)
         # Add relation
         try:
             await model.add_relation(endpoint_1, endpoint_2)
-        except JujuAPIError as e:
-            if "not found" in e.message:
+        except juju.errors.JujuAPIError as e:
+            if self._relation_is_not_found(e):
                 self.log.warning("Relation not found: {}".format(e.message))
                 return
                 self.log.warning("Relation not found: {}".format(e.message))
                 return
-            if "already exists" in e.message:
+            if self._relation_already_exist(e):
                 self.log.warning("Relation already exists: {}".format(e.message))
                 return
             # another exception, raise it
                 self.log.warning("Relation already exists: {}".format(e.message))
                 return
             # another exception, raise it
@@ -810,31 +1379,86 @@ class Libjuju:
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
             await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
+    def _relation_is_not_found(self, juju_error):
+        text = "not found"
+        return (text in juju_error.message) or (
+            juju_error.error_code and text in juju_error.error_code
+        )
+
+    def _relation_already_exist(self, juju_error):
+        text = "already exists"
+        return (text in juju_error.message) or (
+            juju_error.error_code and text in juju_error.error_code
+        )
+
+    async def offer(self, endpoint: RelationEndpoint) -> Offer:
+        """
+        Create an offer from a RelationEndpoint
+
+        :param: endpoint: Relation endpoint
+
+        :return: Offer object
+        """
+        model_name = endpoint.model_name
+        offer_name = f"{endpoint.application_name}-{endpoint.endpoint_name}"
+        controller = await self.get_controller()
+        model = None
+        try:
+            model = await self.get_model(controller, model_name)
+            await model.create_offer(endpoint.endpoint, offer_name=offer_name)
+            offer_list = await self._list_offers(model_name, offer_name=offer_name)
+            if offer_list:
+                return Offer(offer_list[0].offer_url)
+            else:
+                raise Exception("offer was not created")
+        except juju.errors.JujuError as e:
+            if "application offer already exists" not in e.message:
+                raise e
+        finally:
+            if model:
+                self.disconnect_model(model)
+            self.disconnect_controller(controller)
+
     async def consume(
         self,
     async def consume(
         self,
-        offer_url: str,
         model_name: str,
         model_name: str,
-    ):
+        offer: Offer,
+        provider_libjuju: "Libjuju",
+    ) -> str:
         """
         """
-        Adds a remote offer to the model. Relations can be created later using "juju relate".
+        Consumes a remote offer in the model. Relations can be created later using "juju relate".
 
 
-        :param: offer_url:      Offer Url
-        :param: model_name:     Model name
+        :param: model_name:             Model name
+        :param: offer:                  Offer object to consume
+        :param: provider_libjuju:       Libjuju object of the provider endpoint
 
         :raises ParseError if there's a problem parsing the offer_url
         :raises JujuError if remote offer includes and endpoint
         :raises JujuAPIError if the operation is not successful
 
         :raises ParseError if there's a problem parsing the offer_url
         :raises JujuError if remote offer includes and endpoint
         :raises JujuAPIError if the operation is not successful
+
+        :returns: Saas name. It is the application name in the model that reference the remote application.
         """
         """
+        saas_name = f'{offer.name}-{offer.model_name.replace("-", "")}'
+        if offer.vca_id:
+            saas_name = f"{saas_name}-{offer.vca_id}"
         controller = await self.get_controller()
         controller = await self.get_controller()
-        model = await controller.get_model(model_name)
-
+        model = None
+        provider_controller = None
         try:
         try:
-            await model.consume(offer_url)
+            model = await controller.get_model(model_name)
+            provider_controller = await provider_libjuju.get_controller()
+            await model.consume(
+                offer.url, application_alias=saas_name, controller=provider_controller
+            )
+            return saas_name
         finally:
         finally:
-            await self.disconnect_model(model)
+            if model:
+                await self.disconnect_model(model)
+            if provider_controller:
+                await provider_libjuju.disconnect_controller(provider_controller)
             await self.disconnect_controller(controller)
 
             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
 
         """
         Destroy model
 
@@ -846,63 +1470,137 @@ class Libjuju:
         model = None
         try:
             if not await self.model_exists(model_name, controller=controller):
         model = None
         try:
             if not await self.model_exists(model_name, controller=controller):
+                self.log.warn(f"Model {model_name} doesn't exist")
                 return
 
                 return
 
+            self.log.debug(f"Getting model {model_name} to be destroyed")
             model = await self.get_model(controller, model_name)
             model = await self.get_model(controller, model_name)
-            self.log.debug("Destroying model {}".format(model_name))
-            uuid = model.info.uuid
-
+            self.log.debug(f"Destroying manual machines in model {model_name}")
             # Destroy machines that are manually provisioned
             # and still are in pending state
             await self._destroy_pending_machines(model, only_manual=True)
             # 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 self.disconnect_model(model)
 
-            await controller.destroy_model(uuid, force=True, max_wait=0)
+            await asyncio.wait_for(
+                self._destroy_model(model_name, controller),
+                timeout=total_timeout,
+            )
+        except Exception as e:
+            if not await self.model_exists(model_name, controller=controller):
+                self.log.warn(
+                    f"Failed deleting model {model_name}: model doesn't exist"
+                )
+                return
+            self.log.warn(f"Failed deleting model {model_name}: {e}")
+            raise e
+        finally:
+            if model:
+                await self.disconnect_model(model)
+            await self.disconnect_controller(controller)
+
+    async def _destroy_model(
+        self,
+        model_name: str,
+        controller: Controller,
+    ):
+        """
+        Destroy model from controller
+
+        :param: model: Model name to be removed
+        :param: controller: Controller object
+        :param: timeout: Timeout in seconds
+        """
+        self.log.debug(f"Destroying model {model_name}")
+
+        async def _destroy_model_gracefully(model_name: str, controller: Controller):
+            self.log.info(f"Gracefully deleting model {model_name}")
+            resolved = False
+            while model_name in await controller.list_models():
+                if not resolved:
+                    await self.resolve(model_name)
+                    resolved = True
+                await controller.destroy_model(model_name, destroy_storage=True)
+
+                await asyncio.sleep(5)
+            self.log.info(f"Model {model_name} deleted gracefully")
+
+        async def _destroy_model_forcefully(model_name: str, controller: Controller):
+            self.log.info(f"Forcefully deleting model {model_name}")
+            while model_name in await controller.list_models():
+                await controller.destroy_model(
+                    model_name, destroy_storage=True, force=True, max_wait=60
+                )
+                await asyncio.sleep(5)
+            self.log.info(f"Model {model_name} deleted forcefully")
+
+        try:
+            try:
+                await asyncio.wait_for(
+                    _destroy_model_gracefully(model_name, controller), timeout=120
+                )
+            except asyncio.TimeoutError:
+                await _destroy_model_forcefully(model_name, controller)
+        except juju.errors.JujuError as e:
+            if any("has been removed" in error for error in e.errors):
+                return
+            if any("model not found" in error for error in e.errors):
+                return
+            raise e
+
+    async def destroy_application(
+        self, model_name: str, application_name: str, total_timeout: float
+    ):
+        """
+        Destroy application
+
+        :param: model_name:         Model name
+        :param: application_name:   Application name
+        :param: total_timeout:      Timeout
+        """
+
+        controller = await self.get_controller()
+        model = None
 
 
-            # Wait until model is destroyed
-            self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
+        try:
+            model = await self.get_model(controller, model_name)
+            self.log.debug(
+                "Destroying application {} in model {}".format(
+                    application_name, model_name
+                )
+            )
+            application = self._get_application(model, application_name)
+            if application:
+                await application.destroy()
+            else:
+                self.log.warning("Application not found: {}".format(application_name))
 
 
+            self.log.debug(
+                "Waiting for application {} to be destroyed in model {}...".format(
+                    application_name, model_name
+                )
+            )
             if total_timeout is None:
                 total_timeout = 3600
             end = time.time() + total_timeout
             while time.time() < end:
             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:
+                if not self._get_application(model, application_name):
                     self.log.debug(
                     self.log.debug(
-                        "The model {} ({}) was destroyed".format(model_name, uuid)
+                        "The application {} was destroyed in model {} ".format(
+                            application_name, model_name
+                        )
                     )
                     return
                 await asyncio.sleep(5)
             raise Exception(
                     )
                     return
                 await asyncio.sleep(5)
             raise Exception(
-                "Timeout waiting for model {} to be destroyed".format(model_name)
+                "Timeout waiting for application {} to be destroyed in model {}".format(
+                    application_name, model_name
+                )
             )
             )
-        except Exception as e:
-            if model:
-                await self.disconnect_model(model)
-            raise e
         finally:
         finally:
+            if model is not None:
+                await self.disconnect_model(model)
             await self.disconnect_controller(controller)
 
             await self.disconnect_controller(controller)
 
-    async def destroy_application(self, model: Model, application_name: str):
-        """
-        Destroy application
-
-        :param: model:              Model object
-        :param: application_name:   Application name
-        """
-        self.log.debug(
-            "Destroying application {} in model {}".format(
-                application_name, model.info.name
-            )
-        )
-        application = model.applications.get(application_name)
-        if application:
-            await application.destroy()
-        else:
-            self.log.warning("Application not found: {}".format(application_name))
-
     async def _destroy_pending_machines(self, model: Model, only_manual: bool = False):
         """
         Destroy pending machines in a given model
     async def _destroy_pending_machines(self, model: Model, only_manual: bool = False):
         """
         Destroy pending machines in a given model
@@ -946,61 +1644,6 @@ class Libjuju:
                     await self.disconnect_model(model)
                 await self.disconnect_controller(controller)
 
                     await self.disconnect_model(model)
                 await self.disconnect_controller(controller)
 
-    def _get_api_endpoints_db(self) -> [str]:
-        """
-        Get API Endpoints from DB
-
-        :return: List of API endpoints
-        """
-        self.log.debug("Getting endpoints from database")
-
-        juju_info = self.db.get_one(
-            DB_DATA.api_endpoints.table,
-            q_filter=DB_DATA.api_endpoints.filter,
-            fail_on_empty=False,
-        )
-        if juju_info and DB_DATA.api_endpoints.key in juju_info:
-            return juju_info[DB_DATA.api_endpoints.key]
-
-    def _update_api_endpoints_db(self, endpoints: [str]):
-        """
-        Update API endpoints in Database
-
-        :param: List of endpoints
-        """
-        self.log.debug("Saving endpoints {} in database".format(endpoints))
-
-        juju_info = self.db.get_one(
-            DB_DATA.api_endpoints.table,
-            q_filter=DB_DATA.api_endpoints.filter,
-            fail_on_empty=False,
-        )
-        # If it doesn't, then create it
-        if not juju_info:
-            try:
-                self.db.create(
-                    DB_DATA.api_endpoints.table,
-                    DB_DATA.api_endpoints.filter,
-                )
-            except DbException as e:
-                # Racing condition: check if another N2VC worker has created it
-                juju_info = self.db.get_one(
-                    DB_DATA.api_endpoints.table,
-                    q_filter=DB_DATA.api_endpoints.filter,
-                    fail_on_empty=False,
-                )
-                if not juju_info:
-                    raise e
-        self.db.set_one(
-            DB_DATA.api_endpoints.table,
-            DB_DATA.api_endpoints.filter,
-            {DB_DATA.api_endpoints.key: endpoints},
-        )
-
-    def handle_exception(self, loop, context):
-        # All unhandled exceptions by libjuju are handled here.
-        pass
-
     async def health_check(self, interval: float = 300.0):
         """
         Health check to make sure controller and controller_model connections are OK
     async def health_check(self, interval: float = 300.0):
         """
         Health check to make sure controller and controller_model connections are OK
@@ -1035,17 +1678,29 @@ class Libjuju:
         finally:
             await self.disconnect_controller(controller)
 
         finally:
             await self.disconnect_controller(controller)
 
-    async def list_offers(self, model_name: str) -> QueryApplicationOffersResults:
-        """List models with certain names
+    async def _list_offers(
+        self, model_name: str, offer_name: str = None
+    ) -> QueryApplicationOffersResults:
+        """
+        List offers within a model
 
         :param: model_name: Model name
 
         :param: model_name: Model name
+        :param: offer_name: Offer name to filter.
 
 
-        :return:            Returns list of offers
+        :return: Returns application offers results in the model
         """
 
         controller = await self.get_controller()
         try:
         """
 
         controller = await self.get_controller()
         try:
-            return await controller.list_offers(model_name)
+            offers = (await controller.list_offers(model_name)).results
+            if offer_name:
+                matching_offer = []
+                for offer in offers:
+                    if offer.offer_name == offer_name:
+                        matching_offer.append(offer)
+                        break
+                offers = matching_offer
+            return offers
         finally:
             await self.disconnect_controller(controller)
 
         finally:
             await self.disconnect_controller(controller)
 
@@ -1185,23 +1840,67 @@ class Libjuju:
         controller = await self.get_controller()
         try:
             await controller.remove_cloud(name)
         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)
 
         finally:
             await self.disconnect_controller(controller)
 
+    @retry(
+        attempts=20, delay=5, fallback=JujuLeaderUnitNotFound(), callback=retry_callback
+    )
     async def _get_leader_unit(self, application: Application) -> Unit:
         unit = None
         for u in application.units:
             if await u.is_leader_from_status():
                 unit = u
                 break
     async def _get_leader_unit(self, application: Application) -> Unit:
         unit = None
         for u in application.units:
             if await u.is_leader_from_status():
                 unit = u
                 break
+        if not unit:
+            raise Exception()
         return unit
 
         return unit
 
-    async def get_cloud_credentials(self, cloud_name: str, credential_name: str):
+    async def get_cloud_credentials(self, cloud: Cloud) -> typing.List:
+        """
+        Get cloud credentials
+
+        :param: cloud: Cloud object. The returned credentials will be from this cloud.
+
+        :return: List of credentials object associated to the specified cloud
+
+        """
         controller = await self.get_controller()
         try:
             facade = client.CloudFacade.from_connection(controller.connection())
         controller = await self.get_controller()
         try:
             facade = client.CloudFacade.from_connection(controller.connection())
-            cloud_cred_tag = tag.credential(cloud_name, self.username, credential_name)
+            cloud_cred_tag = tag.credential(
+                cloud.name, self.vca_connection.data.user, cloud.credential_name
+            )
             params = [client.Entity(cloud_cred_tag)]
             return (await facade.Credential(params)).results
         finally:
             await self.disconnect_controller(controller)
             params = [client.Entity(cloud_cred_tag)]
             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)