From: Dario Faccin Date: Thu, 6 Jul 2023 06:47:38 +0000 (+0200) Subject: OSMENG-1098: Perform unit resolve without retry X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=ea6f8172f4c3c188c99474468975bab3abc4e94e;p=osm%2FLCM.git OSMENG-1098: Perform unit resolve without retry Change-Id: Icef1d67bbf4af6efec13712aec3efd88982dfd1e Signed-off-by: Dario Faccin --- diff --git a/osm_lcm/nglcm.py b/osm_lcm/nglcm.py index 926fe79..1d60e6e 100644 --- a/osm_lcm/nglcm.py +++ b/osm_lcm/nglcm.py @@ -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), diff --git a/osm_lcm/temporal/juju_paas_activities.py b/osm_lcm/temporal/juju_paas_activities.py index a5c4dfb..dfa3115 100644 --- a/osm_lcm/temporal/juju_paas_activities.py +++ b/osm_lcm/temporal/juju_paas_activities.py @@ -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) diff --git a/osm_lcm/tests/test_juju_paas_activities.py b/osm_lcm/tests/test_juju_paas_activities.py index b3913c0..1381c33 100644 --- a/osm_lcm/tests/test_juju_paas_activities.py +++ b/osm_lcm/tests/test_juju_paas_activities.py @@ -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, + )