diff --git a/juju/client/connection.py b/juju/client/connection.py index 9a4976af6..0ee2b25b6 100644 --- a/juju/client/connection.py +++ b/juju/client/connection.py @@ -241,6 +241,7 @@ class Connection: MAX_FRAME_SIZE = 2**22 "Maximum size for a single frame. Defaults to 4MB." + monitor: Monitor @classmethod async def connect( diff --git a/juju/model.py b/juju/model.py index 9ffd8c223..b551d7f3d 100644 --- a/juju/model.py +++ b/juju/model.py @@ -29,6 +29,7 @@ Optional, overload, Set, + Tuple, TypeVar, TYPE_CHECKING, # Union, @@ -38,6 +39,7 @@ import yaml import websockets +import juju.status from . import provisioner, tag, utils, jasyncio from .annotationhelper import _get_annotations, _set_annotations from .bundle import BundleHandler, get_charm_series, is_local_charm @@ -929,7 +931,9 @@ def add_local_charm(self, charm_file, series="", size=None): instead. """ - conn, headers, path_prefix = self.connection().https_connection() + connection = self.connection() + assert connection + conn, headers, path_prefix = connection.https_connection() path = "%s/charms?series=%s" % (path_prefix, series) headers['Content-Type'] = 'application/zip' if size: @@ -1313,12 +1317,14 @@ async def _all_watcher(): del allwatcher.Id continue except websockets.ConnectionClosed: - monitor = self.connection().monitor + connection = self.connection() + assert connection + monitor = connection.monitor if monitor.status == monitor.ERROR: # closed unexpectedly, try to reopen log.warning( 'Watcher: connection closed, reopening') - await self.connection().reconnect() + await connection.reconnect() if monitor.status != monitor.CONNECTED: # reconnect failed; abort and shutdown log.error('Watcher: automatic reconnect ' @@ -3062,13 +3068,11 @@ async def _check_idle( logging.info("Waiting for app %r", app_name) return False - reveal_type(app) - reveal_type(app.status) - assert app.status - if app.status.status == "error" and raise_on_error: - raise JujuAppError(f"App {app_name!r} is in error: {app.status.info!r}") - if app.status.status == "blocked" and raise_on_blocked: - raise JujuAppError(f"App {app_name!r} is blocked: {app.status.info!r}") + app_status, app_info = self._compound_status(full_status, app_name) + if app_status == "error" and raise_on_error: + raise JujuAppError(f"App {app_name!r} is in error: {app_info!r}") + if app_status == "blocked" and raise_on_blocked: + raise JujuAppError(f"App {app_name!r} is blocked: {app_info!r}") # FIXME the old code treats app status "unset" as special # and uses max(unit statuses) instead (see Application.status) @@ -3107,6 +3111,40 @@ async def _check_idle( return True + def _compound_status(self, full_status: FullStatus, app_name: str) -> Tuple[str, str]: + app = full_status.applications[app_name] + assert app + assert app.status + assert isinstance(app.status.status, str) + assert isinstance(app.status.info, str) + app_status = app.status.status + app_info = app.status.info + unit_details: List[Tuple[str, str]] = [] + + for unit_name, unit in app.units.items(): + assert unit + assert unit.agent_status + assert isinstance(unit.agent_status.status, str) + assert isinstance(unit.agent_status.info, str) + unit_details.append(( + unit.agent_status.status, + f"({unit_name}) {unit.agent_status.info}", + )) + + assert unit.workload_status + assert isinstance(unit.workload_status.status, str) + assert isinstance(unit.workload_status.info, str) + unit_details.append(( + unit.workload_status.status, + f"({unit_name}) {unit.workload_status.info}", + )) + + unit_details.sort(key=lambda t: -juju.status.severity_map.get(t[0], 0)) + if app_status == "unset" and unit_details: + return unit_details[0] + else: + return app_status, app_info + async def _check_idle_unit( self, unit_name: str, diff --git a/juju/status.py b/juju/status.py index 46485dac6..8a846bfaf 100644 --- a/juju/status.py +++ b/juju/status.py @@ -18,15 +18,15 @@ def derive_status(statues): current = 'unknown' for status in statues: - if status in serverities and serverities[status] > serverities[current]: + if status in severity_map and severity_map[status] > severity_map[current]: current = status return current -""" serverities holds status values with a severity measure. +""" severity_map holds status values with a severity measure. Status values with higher severity are used in preference to others. """ -serverities = { +severity_map = { 'error': 100, 'blocked': 90, 'waiting': 80, diff --git a/tests/unit/test_wait_for_idle.py b/tests/unit/test_wait_for_idle.py index b6f50e482..f9f621628 100644 --- a/tests/unit/test_wait_for_idle.py +++ b/tests/unit/test_wait_for_idle.py @@ -11,7 +11,12 @@ from juju.application import Application from juju.client.facade import _convert_response -from juju.client._definitions import FullStatus +from juju.client._definitions import ( + ApplicationStatus, + DetailedStatus, + FullStatus, + UnitStatus, +) from juju.errors import JujuAppError from juju.machine import Machine from juju.model import Model @@ -106,6 +111,35 @@ async def test_after_idle_waiting_for_unit(full_status_response: dict, kwargs: D assert not idle and not legacy +def test_compound_status_no_units(): + fs = FullStatus() + fs.applications["test"] = app = ApplicationStatus() + app.status = DetailedStatus(status="unset", info="who knows?") + assert app.units is not None + assert not app.units + assert ModelFake()._compound_status(fs, "test") == ("unset", "who knows?") + + +def test_compound_status_unset(): + fs = FullStatus() + fs.applications["test"] = app = ApplicationStatus() + app.status = DetailedStatus(status="unset", info="who knows?") + app.units["test/0"] = unit = UnitStatus() + unit.agent_status = DetailedStatus(status="waiting", info="w") + unit.workload_status = DetailedStatus(status="blocked", info="b") + assert ModelFake()._compound_status(fs, "test") == ("blocked", "(test/0) b") + + +def test_compound_status_unknown(): + fs = FullStatus() + fs.applications["test"] = app = ApplicationStatus() + app.status = DetailedStatus(status="unknown", info="who knows?") + app.units["test/0"] = unit = UnitStatus() + unit.agent_status = DetailedStatus(status="waiting", info="w") + unit.workload_status = DetailedStatus(status="blocked", info="b") + assert ModelFake()._compound_status(fs, "test") == ("unknown", "who knows?") + + @pytest.fixture def kwargs() -> Dict[str, Any]: return dict(