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,
CreateModel,
DeployCharm,
RemoveCharm,
+ ResolveCharmErrors,
)
from osm_common.temporal.dataclasses_common import (
CharmInfo,
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)
CheckCharmStatusImpl,
CheckCharmIsRemovedImpl,
RemoveCharmImpl,
+ ResolveCharmErrorsImpl,
)
from osm_common.temporal.workflows.vdu import VduInstantiateWorkflow
from osm_common.temporal.dataclasses_common import CharmInfo, VduComputeConstraints
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,
+ )