OSMENG-1098: Perform unit resolve without retry 43/13643/12
authorDario Faccin <dario.faccin@canonical.com>
Thu, 6 Jul 2023 06:47:38 +0000 (08:47 +0200)
committerbeierlm <mark.beierl@canonical.com>
Mon, 17 Jul 2023 18:04:09 +0000 (20:04 +0200)
Change-Id: Icef1d67bbf4af6efec13712aec3efd88982dfd1e
Signed-off-by: Dario Faccin <dario.faccin@canonical.com>
osm_lcm/nglcm.py
osm_lcm/temporal/juju_paas_activities.py
osm_lcm/tests/test_juju_paas_activities.py

index 926fe79..1d60e6e 100644 (file)
@@ -38,6 +38,7 @@ from osm_lcm.temporal.juju_paas_activities import (
     TestVimConnectivityImpl,
     RemoveCharmImpl,
     CheckCharmIsRemovedImpl,
+    ResolveCharmErrorsImpl,
 )
 
 from osm_lcm.temporal.lcm_activities import NsLcmNoOpImpl, UpdateNsLcmOperationStateImpl
@@ -188,6 +189,7 @@ class NGLcm:
             TestVimConnectivityImpl(paas_connector_instance),
             RemoveCharmImpl(paas_connector_instance),
             CheckCharmIsRemovedImpl(paas_connector_instance),
+            ResolveCharmErrorsImpl(paas_connector_instance),
             UpdateVimOperationStateImpl(self.db),
             UpdateVimStateImpl(self.db),
             DeleteVimRecordImpl(self.db),
index a5c4dfb..dfa3115 100644 (file)
@@ -20,6 +20,7 @@ from dataclasses import dataclass
 
 from juju.application import Application
 from juju.controller import Controller
+from juju.model import Model
 from n2vc.config import EnvironConfig
 from osm_common.temporal.activities.paas import (
     TestVimConnectivity,
@@ -28,6 +29,7 @@ from osm_common.temporal.activities.paas import (
     CreateModel,
     DeployCharm,
     RemoveCharm,
+    ResolveCharmErrors,
 )
 from osm_common.temporal.dataclasses_common import (
     CharmInfo,
@@ -304,3 +306,35 @@ class CheckCharmIsRemovedImpl(CheckCharmIsRemoved):
         while app_name in model.applications:
             activity.heartbeat()
             await asyncio.sleep(activity_input.poll_interval)
+
+
+@activity.defn(name=ResolveCharmErrors.__name__)
+class ResolveCharmErrorsImpl(ResolveCharmErrors):
+    async def __call__(self, activity_input: ResolveCharmErrors.Input) -> None:
+        model_name: str = activity_input.model_name
+        application_name: str = activity_input.application_name
+        controller: Controller = await self.juju_controller._get_controller(
+            activity_input.vim_uuid
+        )
+        if model_name not in (await controller.list_models()):
+            return
+        model: Model = await controller.get_model(model_name)
+        if application_name not in model.applications:
+            return
+        application: Application = model.applications[application_name]
+        while not await ResolveCharmErrorsImpl.is_error_resolved(application):
+            await self.resolve_error(application)
+            activity.heartbeat()
+            await asyncio.sleep(activity_input.poll_interval)
+
+    @staticmethod
+    async def is_error_resolved(application) -> bool:
+        return application.status != "error"
+
+    async def resolve_error(self, application) -> None:
+        for unit in application.units:
+            if unit.workload_status == "error":
+                self.logger.debug(
+                    f"Application `{application.entity_id}`, Unit `{unit.entity_id}` in error state, resolving"
+                )
+                await unit.resolved(retry=False)
index b3913c0..1381c33 100644 (file)
@@ -32,6 +32,7 @@ from osm_lcm.temporal.juju_paas_activities import (
     CheckCharmStatusImpl,
     CheckCharmIsRemovedImpl,
     RemoveCharmImpl,
+    ResolveCharmErrorsImpl,
 )
 from osm_common.temporal.workflows.vdu import VduInstantiateWorkflow
 from osm_common.temporal.dataclasses_common import CharmInfo, VduComputeConstraints
@@ -577,3 +578,171 @@ class TestCheckCharmIsRemoved(TestJujuPaasActivitiesBase):
 
         await self.env.run(self.check_charm_is_removed.__call__, arg)
         self.controller.get_model.assert_not_called()
+
+
+class TestResolveCharmErrors(TestJujuPaasActivitiesBase):
+    app_name = "my_app_name"
+
+    def setUp(self) -> None:
+        super().setUp()
+        self.env.on_heartbeat = self.on_heartbeat
+        self.heartbeat_count = 0
+        self.heartbeat_maximum = 5
+        self.resolve_charm = ResolveCharmErrorsImpl(self.juju_paas)
+
+    def on_heartbeat(self, *args, **kwargs):
+        self.heartbeat_count += 1
+        if self.heartbeat_count > self.heartbeat_maximum:
+            self.env.cancel()
+
+    @mock.patch(
+        "osm_lcm.temporal.juju_paas_activities.ResolveCharmErrorsImpl.resolve_error"
+    )
+    async def test_resolve_charm__application_exists_in_error__error_resolved(
+        self, mock_resolve_error
+    ):
+        """Initially, application status is in error, then it's resolved in second try."""
+        resolve_charm_input = ResolveCharmErrorsImpl.Input(
+            vim_uuid=vim_content["_id"],
+            application_name=self.app_name,
+            model_name=namespace,
+            poll_interval=0,
+        )
+        self.add_application(self.app_name)
+        self.controller.list_models.return_value = [namespace]
+        type(self.application).status = mock.PropertyMock(
+            side_effect=["error", "error", "active"]
+        )
+        await self.env.run(
+            self.resolve_charm.__call__,
+            resolve_charm_input,
+        )
+        self.assertEqual(mock_resolve_error.call_count, 2)
+
+    @mock.patch(
+        "osm_lcm.temporal.juju_paas_activities.ResolveCharmErrorsImpl.resolve_error"
+    )
+    async def test_resolve_charm__application_exists_in_error__error_is_not_resolved(
+        self, mock_resolve_error
+    ):
+        """Error is not resolved, activity cancelled."""
+        resolve_charm_input = ResolveCharmErrorsImpl.Input(
+            vim_uuid=vim_content["_id"],
+            application_name=self.app_name,
+            model_name=namespace,
+            poll_interval=0,
+        )
+        self.add_application(self.app_name)
+        self.controller.list_models.return_value = [namespace]
+        type(self.application).status = mock.PropertyMock(return_value="error")
+        with self.assertRaises(asyncio.exceptions.CancelledError):
+            await self.env.run(
+                self.resolve_charm.__call__,
+                resolve_charm_input,
+            )
+
+    @mock.patch(
+        "osm_lcm.temporal.juju_paas_activities.ResolveCharmErrorsImpl.resolve_error"
+    )
+    async def test_resolve_charm__application_exists_in_active__no_need_to_resolve(
+        self, mock_resolve_error
+    ):
+        resolve_charm_input = ResolveCharmErrorsImpl.Input(
+            vim_uuid=vim_content["_id"],
+            application_name=self.app_name,
+            model_name=namespace,
+            poll_interval=0,
+        )
+        self.add_application(self.app_name)
+        self.controller.list_models.return_value = [namespace]
+        type(self.application).status = mock.PropertyMock(
+            side_effect=["active", "active"]
+        )
+        await self.env.run(
+            self.resolve_charm.__call__,
+            resolve_charm_input,
+        )
+        mock_resolve_error.assert_not_called()
+
+    @mock.patch(
+        "osm_lcm.temporal.juju_paas_activities.ResolveCharmErrorsImpl.resolve_error"
+    )
+    async def test_resolve_charm__application_does_not_exist__no_need_to_resolve(
+        self, mock_resolve_error
+    ):
+        """Another application exists but required application does not exist."""
+        resolve_charm_input = ResolveCharmErrorsImpl.Input(
+            vim_uuid=vim_content["_id"],
+            application_name=self.app_name,
+            model_name=namespace,
+            poll_interval=0,
+        )
+        self.add_application("other-app")
+        self.controller.list_models.return_value = [namespace]
+        type(self.application).status = mock.PropertyMock(side_effect=["error"])
+        await self.env.run(
+            self.resolve_charm.__call__,
+            resolve_charm_input,
+        )
+        mock_resolve_error.assert_not_called()
+
+    @mock.patch(
+        "osm_lcm.temporal.juju_paas_activities.ResolveCharmErrorsImpl.resolve_error"
+    )
+    async def test_resolve_charm__model_does_not_exist__no_need_to_resolve(
+        self, mock_resolve_error
+    ):
+        """Another model exists but required model does not exist."""
+        resolve_charm_input = ResolveCharmErrorsImpl.Input(
+            vim_uuid=vim_content["_id"],
+            application_name=self.app_name,
+            model_name=namespace,
+            poll_interval=0,
+        )
+        self.add_application(self.app_name)
+        self.controller.list_models.return_value = ["other-namespace"]
+        type(self.application).status = mock.PropertyMock(side_effect=["error"])
+        await self.env.run(
+            self.resolve_charm.__call__,
+            resolve_charm_input,
+        )
+        mock_resolve_error.assert_not_called()
+
+    @parameterized.expand(
+        [
+            (
+                "application_in_error",
+                "error",
+                False,
+            ),
+            (
+                "application_in_active",
+                "active",
+                True,
+            ),
+        ]
+    )
+    async def test_is_error_resolved(self, _, status, expected_result):
+        self.add_application(self.app_name)
+        self.application.status = status
+        result = await ResolveCharmErrorsImpl.is_error_resolved(self.application)
+        self.assertEqual(result, expected_result)
+
+    async def test_is_error_resolved__application_with_multiple_units_only_one_in_error__resolve_required(
+        self,
+    ):
+        self.add_application(self.app_name)
+        unit_active = Mock(spec=Unit)
+        self.application.entity_id = "my-application"
+        unit_error = Mock(spec=Unit)
+        unit_active.workload_status = "active"
+        unit_active.entity_id = "my-unit-0"
+        unit_error.workload_status = "error"
+        unit_error.entity_id = "my-unit-1"
+        self.application.units = [unit_active, unit_error]
+        self.application.status = "error"
+        await self.resolve_charm.resolve_error(application=self.application)
+        unit_active.resolved.assert_not_called()
+        unit_error.resolved.assert_called_once_with(
+            retry=False,
+        )