Skip to content

Commit

Permalink
Add cache property for applications to COUModel (#275)
Browse files Browse the repository at this point in the history
Like this it will be possible to access all application in model,
without calling get_applications. This will be used in planning stage,
where we are not running async functions and for any app, which require
using other applications. e.g. ceph-osd need to check nova-compute
status.

With this feature, we can easily access all colocated nova-compute and
ceph-mon during pre- and post-upgrade steps in ceph-osd.
  • Loading branch information
rgildein committed Mar 7, 2024
1 parent d1f584d commit 604984a
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 25 deletions.
64 changes: 41 additions & 23 deletions cou/utils/juju_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ def __init__(self, name: Optional[str]):
self._juju_data = FileJujuData()
self._model = Model(max_frame_size=JUJU_MAX_FRAME_SIZE, jujudata=self.juju_data)
self._name = name
self._applications: dict[str, COUApplication] | None = None

@property
def applications(self) -> dict[str, COUApplication]:
"""Return cached applications.
This property represents the cached output of the get_applications function.
:return: dictionary of COUApplication
:rtype: dict[str, COUApplication]
:raises ValueError: When get_applications has not yet been called.
"""
if self._applications is None:
raise ValueError("The get_applications has not yet been called.")

return self._applications

@property
def connected(self) -> bool:
Expand Down Expand Up @@ -266,6 +282,27 @@ async def _get_application(self, name: str) -> Application:

return app

async def _get_machines(self) -> dict[str, COUMachine]:
"""Get all the machines in the model.
:return: Dictionary of the machines found in the model. E.g: {'0': Machine0}
:rtype: dict[str, Machine]
"""
model = await self._get_model()

return {
machine.id: COUMachine(
machine_id=machine.id,
apps=tuple(
unit.application
for unit in self._model.units.values()
if unit.machine.id == machine.id
),
az=machine.hardware_characteristics.get("availability-zone"),
)
for machine in model.machines.values()
}

async def _get_model(self) -> Model:
"""Get juju.model.Model and make sure that it is connected.
Expand Down Expand Up @@ -325,9 +362,9 @@ async def get_applications(self) -> dict[str, COUApplication]:
# note(rgildein): We get the applications from the Juju status, since we can get more
# information the status than from objects. e.g. workload_version for unit
full_status = await self.get_status()
machines = await self.get_machines()
machines = await self._get_machines()

return {
self._applications = {
app: COUApplication(
name=app,
can_upgrade_to=status.can_upgrade_to,
Expand All @@ -348,6 +385,8 @@ async def get_applications(self) -> dict[str, COUApplication]:
for app, status in full_status.applications.items()
}

return self._applications

@retry(no_retry_exceptions=(ApplicationNotFound,))
async def get_application_config(self, name: str) -> dict:
"""Return application configuration.
Expand Down Expand Up @@ -376,27 +415,6 @@ async def get_charm_name(self, application_name: str) -> str:

return app.charm_name

async def get_machines(self) -> dict[str, COUMachine]:
"""Get all the machines in the model.
:return: Dictionary of the machines found in the model. E.g: {'0': Machine0}
:rtype: dict[str, Machine]
"""
model = await self._get_model()

return {
machine.id: COUMachine(
machine_id=machine.id,
apps=tuple(
unit.application
for unit in self._model.units.values()
if unit.machine.id == machine.id
),
az=machine.hardware_characteristics.get("availability-zone"),
)
for machine in model.machines.values()
}

@retry
async def get_status(self) -> FullStatus:
"""Return the full juju status output.
Expand Down
29 changes: 27 additions & 2 deletions tests/unit/utils/test_juju_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,22 @@ def test_coumodel_init(mock_juju_data, mocker):
assert model._model == mocked_model


def test_coumodel_applications_no_cached(mocked_model):
"""Test COUModel applications property without calling get_applications."""
model = juju_utils.COUModel("test-model")

with pytest.raises(ValueError, match="The get_applications has not yet been called."):
model.applications


def test_coumodel_applications(mocked_model):
"""Test COUModel applications property without calling get_applications."""
model = juju_utils.COUModel("test-model")
model._applications = exp_apps = {"app1": MagicMock(spec_set=juju_utils.COUApplication)()}

assert model.applications == exp_apps


def test_coumodel_connected_no_connection(mocked_model):
"""Test COUModel connected property."""
mocked_model.connection.side_effect = NoConnectionException
Expand Down Expand Up @@ -581,7 +597,7 @@ async def test_get_machines(mocked_model):
}

model = juju_utils.COUModel("test-model")
machines = await model.get_machines()
machines = await model._get_machines()

assert machines == expected_machines

Expand Down Expand Up @@ -621,7 +637,7 @@ def _generate_app_status(units: dict[str, MagicMock]) -> MagicMock:

@pytest.mark.asyncio
@patch("cou.utils.juju_utils.COUModel.get_status")
@patch("cou.utils.juju_utils.COUModel.get_machines")
@patch("cou.utils.juju_utils.COUModel._get_machines")
async def test_get_applications(mock_get_machines, mock_get_status, mocked_model):
"""Test COUModel getting applications from model.
Expand Down Expand Up @@ -697,3 +713,12 @@ async def test_get_applications(mock_get_machines, mock_get_status, mocked_model
mock_get_machines.assert_awaited_once_with()
(mocked_model.applications[app].assert_awaited_once_with(app) for app in full_status_apps)
assert apps == exp_apps

# checking that apps were cached
mock_get_status.reset_mock()
mock_get_machines.reset_mock()

assert model.applications == exp_apps

mock_get_status.assert_not_awaited()
mock_get_machines.assert_not_awaited()

0 comments on commit 604984a

Please sign in to comment.