Give a warning when removing a cloud that does not exist
[osm/N2VC.git] / n2vc / libjuju.py
index cb2e0ba..1cb1e9e 100644 (file)
@@ -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:
@@ -273,7 +273,9 @@ class Libjuju:
         """
         return await controller.get_model(model_name)
 
-    async def model_exists(self, model_name: str, controller: Controller = None) -> bool:
+    async def model_exists(
+        self, model_name: str, controller: Controller = None
+    ) -> bool:
         """
         Check if model exists
 
@@ -343,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):
         """
@@ -662,11 +664,11 @@ class Libjuju:
         return application
 
     async def scale_application(
-            self,
-            model_name: str,
-            application_name: str,
-            scale: int = 1,
-            total_timeout: float = None,
+        self,
+        model_name: str,
+        application_name: str,
+        scale: int = 1,
+        total_timeout: float = None,
     ):
         """
         Scale application (K8s)
@@ -693,8 +695,7 @@ class Libjuju:
             await application.scale(scale=scale)
             # Wait until application is scaled in model
             self.log.debug(
-                "Waiting for application {} to be scaled in model {}...".format
-                (
+                "Waiting for application {} to be scaled in model {}...".format(
                     application_name, model_name
                 )
             )
@@ -707,7 +708,9 @@ class Libjuju:
                 # 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)
+                    await JujuModelWatcher.wait_for_model(
+                        model=model, timeout=total_timeout
+                    )
                     self.log.debug(
                         "Application {} is scaled in model {}".format(
                             application_name, model_name
@@ -756,7 +759,7 @@ class Libjuju:
         db_dict: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
-        **kwargs
+        **kwargs,
     ):
         """Execute action
 
@@ -918,7 +921,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
@@ -955,7 +958,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
 
@@ -969,42 +972,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)
 
-            # Wait until model is destroyed
-            self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
+    async def _destroy_model(
+        self, model_name: str, controller: Controller, timeout: float = 1800
+    ):
+        """
+        Destroy model from controller
 
-            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
+        :param: model: Model name to be removed
+        :param: controller: Controller object
+        :param: timeout: Timeout in seconds
+        """
+
+        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
@@ -1291,6 +1302,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)