From be6825b9c4fdbe4576fe304cbe647e452d002e31 Mon Sep 17 00:00:00 2001 From: Gabriel Cocenza Date: Mon, 22 Jan 2024 21:31:32 -0300 Subject: [PATCH 1/6] Machine Class - Introduction of the machine class with the following fields: - hostname, az, id and is_data_plane - juju_utils generate COU machine class based on juju Application data - Apps receive the machines with units deployed as attribute during the creation of the instance. - changed conftests to have a single source of truth for the unit tests. --- cou/apps/base.py | 6 +- cou/apps/factory.py | 14 +- cou/apps/machine.py | 36 ++ cou/steps/analyze.py | 38 +- cou/utils/juju_utils.py | 36 +- tests/unit/apps/test_core.py | 172 ++++--- tests/unit/apps/test_machines.py | 56 +++ tests/unit/conftest.py | 769 +++++++++++++++++-------------- 8 files changed, 692 insertions(+), 435 deletions(-) create mode 100644 cou/apps/machine.py create mode 100644 tests/unit/apps/test_machines.py diff --git a/cou/apps/base.py b/cou/apps/base.py index 06a883e6..64dfca1f 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -24,6 +24,7 @@ from juju.client._definitions import ApplicationStatus, UnitStatus from ruamel.yaml import YAML +from cou.apps.machine import Machine from cou.exceptions import ( ApplicationError, HaltUpgradePlanGeneration, @@ -55,8 +56,8 @@ class ApplicationUnit: name: str os_version: OpenStackRelease + machine: Machine workload_version: str = "" - machine: str = "" @dataclass @@ -100,6 +101,7 @@ class OpenStackApplication: config: dict model: COUModel charm: str + machines: dict[str, Machine] charm_origin: str = "" os_origin: str = "" origin_setting: Optional[str] = None @@ -170,7 +172,7 @@ def _populate_units(self) -> None: name=name, workload_version=unit.workload_version, os_version=compatible_os_version, - machine=unit.machine, + machine=self.machines[unit.machine], ) ) diff --git a/cou/apps/factory.py b/cou/apps/factory.py index 2136f634..e57a4afd 100644 --- a/cou/apps/factory.py +++ b/cou/apps/factory.py @@ -21,7 +21,7 @@ from juju.client._definitions import ApplicationStatus -from cou.apps.base import OpenStackApplication +from cou.apps.base import Machine, OpenStackApplication from cou.utils.juju_utils import COUModel from cou.utils.openstack import is_charm_supported @@ -41,6 +41,7 @@ def create( config: dict, model: COUModel, charm: str, + machines: dict[str, Machine], ) -> Optional[OpenStackApplication]: """Create the OpenStackApplication or registered subclasses. @@ -48,6 +49,8 @@ def create( decorator can be instantiated and used with their customized methods. :param name: Name of the application :type name: str + :param machines: Machines in the model + :type machines: dict[str, Machine] :param status: Status of the application :type status: ApplicationStatus :param config: Configuration of the application @@ -62,7 +65,14 @@ def create( # pylint: disable=too-many-arguments if is_charm_supported(charm): app_class = cls.charms.get(charm, OpenStackApplication) - return app_class(name=name, status=status, config=config, model=model, charm=charm) + return app_class( + name=name, + status=status, + config=config, + model=model, + charm=charm, + machines=machines, + ) logger.debug( "'%s' is not a supported OpenStack related application and will be ignored.", name, diff --git a/cou/apps/machine.py b/cou/apps/machine.py new file mode 100644 index 00000000..5f99cedc --- /dev/null +++ b/cou/apps/machine.py @@ -0,0 +1,36 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Machine class.""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True) +class Machine: + """Representation of a juju machine.""" + + machine_id: str + hostname: str + az: Optional[str] # simple deployments may not have azs + is_data_plane: bool + + def __repr__(self) -> str: + """Representation of the juju Machine. + + :return: Representation of the juju Machine + :rtype: str + """ + return f"Machine[{self.machine_id}]" diff --git a/cou/steps/analyze.py b/cou/steps/analyze.py index f8517884..633d769b 100644 --- a/cou/steps/analyze.py +++ b/cou/steps/analyze.py @@ -22,7 +22,7 @@ from cou.apps.base import OpenStackApplication from cou.apps.factory import AppFactory from cou.utils import juju_utils -from cou.utils.openstack import DATA_PLANE_CHARMS, UPGRADE_ORDER, OpenStackRelease +from cou.utils.openstack import UPGRADE_ORDER, OpenStackRelease logger = logging.getLogger(__name__) @@ -39,6 +39,8 @@ class Analysis: :type apps_data_plane: list[OpenStackApplication] """ + # pylint: disable=too-many-instance-attributes + model: juju_utils.COUModel apps_control_plane: list[OpenStackApplication] apps_data_plane: list[OpenStackApplication] @@ -66,25 +68,9 @@ def _split_apps( :return: Control plane and data plane application lists. :rtype: tuple[list[OpenStackApplication], list[OpenStackApplication]] """ - - def is_data_plane(app: OpenStackApplication) -> bool: - """Check if app belong to data plane. - - :param app: application - :type app: OpenStackApplication - :return: boolean - :rtype: bool - """ - return app.charm in DATA_PLANE_CHARMS - control_plane, data_plane = [], [] - data_plane_machines = { - unit.machine for app in apps if is_data_plane(app) for unit in app.units - } for app in apps: - if is_data_plane(app): - data_plane.append(app) - elif any(unit.machine in data_plane_machines for unit in app.units): + if any(unit.machine.is_data_plane for unit in app.units): data_plane.append(app) else: control_plane.append(app) @@ -105,7 +91,11 @@ async def create(cls, model: juju_utils.COUModel) -> Analysis: control_plane, data_plane = cls._split_apps(apps) - return Analysis(model=model, apps_data_plane=data_plane, apps_control_plane=control_plane) + return Analysis( + model=model, + apps_data_plane=data_plane, + apps_control_plane=control_plane, + ) @classmethod async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplication]: @@ -121,13 +111,19 @@ async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplicatio :rtype: List[OpenStackApplication] """ juju_status = await model.get_status() + juju_applications = await model.get_applications() + juju_machines = await model.get_model_machines() apps = { AppFactory.create( name=app, status=app_status, - config=await model.get_application_config(app), + config=juju_applications[app].get_config(), model=model, - charm=await model.get_charm_name(app), + charm=juju_applications[app].charm_name, + machines={ + unit_status.machine: juju_machines[unit_status.machine] + for unit_status in app_status.units.values() + }, ) for app, app_status in juju_status.applications.items() if app_status diff --git a/cou/utils/juju_utils.py b/cou/utils/juju_utils.py index d4c2bc23..f2f023ef 100644 --- a/cou/utils/juju_utils.py +++ b/cou/utils/juju_utils.py @@ -30,6 +30,7 @@ from macaroonbakery.httpbakery import BakeryException from six import wraps +from cou.apps.machine import Machine from cou.exceptions import ( ActionFailed, ApplicationError, @@ -39,7 +40,7 @@ UnitNotFound, WaitForApplicationsTimeout, ) -from cou.utils.openstack import is_charm_supported +from cou.utils.openstack import DATA_PLANE_CHARMS, is_charm_supported JUJU_MAX_FRAME_SIZE: int = 2**30 DEFAULT_TIMEOUT: int = int(os.environ.get("COU_TIMEOUT", 10)) @@ -190,6 +191,15 @@ async def _get_action_result(self, action: Action, raise_on_failure: bool) -> Ac return result + async def get_applications(self) -> dict[str, Application]: + """Get all applications from the model. + + :return: Applications from the model connected. + :rtype: dict[str, Application] + """ + model = await self._get_model() + return model.applications + async def _get_application(self, name: str) -> Application: """Get juju.application.Application from model. @@ -499,3 +509,27 @@ async def _wait_for_active_idle() -> None: apps = await self._get_supported_apps() await _wait_for_active_idle() + + async def get_model_machines(self) -> dict[str, Machine]: + """Get all the machines in the model. + + :return: Dictionary of the machines found in the model. E.g: {'0': Machine0} + :rtype: dict[str, Machine] + """ + juju_applications = await self.get_applications() + machines: dict[str, Machine] = {} + for app in juju_applications.values(): + charm_name = app.charm_name + for unit in app.units: + unit_machine = unit.machine.data + machine = machines.get( + unit_machine["id"], + Machine( + machine_id=unit_machine["id"], + hostname=unit_machine["hostname"], + az=unit_machine["hardware-characteristic"].get("availability-zone"), + is_data_plane=charm_name in DATA_PLANE_CHARMS, + ), + ) + machines[unit_machine["id"]] = machine + return machines diff --git a/tests/unit/apps/test_core.py b/tests/unit/apps/test_core.py index 99056091..71e48c88 100644 --- a/tests/unit/apps/test_core.py +++ b/tests/unit/apps/test_core.py @@ -33,15 +33,36 @@ from tests.unit.apps.utils import add_steps -def test_application_eq(status, config, model): +def test_application_eq(status, config, model, apps_machines): """Name of the app is used as comparison between Applications objects.""" - status_keystone_1 = status["keystone_ussuri"] + status_keystone_1 = status["keystone_focal_ussuri"] config_keystone_1 = config["openstack_ussuri"] - status_keystone_2 = status["keystone_wallaby"] + status_keystone_2 = status["keystone_focal_wallaby"] config_keystone_2 = config["openstack_wallaby"] - keystone_1 = Keystone("keystone", status_keystone_1, config_keystone_1, model, "keystone") - keystone_2 = Keystone("keystone", status_keystone_2, config_keystone_2, model, "keystone") - keystone_3 = Keystone("keystone_foo", status_keystone_1, config_keystone_1, model, "keystone") + keystone_1 = Keystone( + "keystone", + status_keystone_1, + config_keystone_1, + model, + "keystone", + apps_machines["keystone"], + ) + keystone_2 = Keystone( + "keystone", + status_keystone_2, + config_keystone_2, + model, + "keystone", + apps_machines["keystone"], + ) + keystone_3 = Keystone( + "keystone_foo", + status_keystone_1, + config_keystone_1, + model, + "keystone", + apps_machines["keystone"], + ) # keystone_1 is equal to keystone_2 because they have the same name # even if they have different status and config. @@ -92,9 +113,9 @@ def assert_application( assert app.is_valid_track(app.channel) == exp_is_valid_track -def test_application_ussuri(status, config, units, model): +def test_application_ussuri(status, config, units, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] exp_is_from_charm_store = False exp_os_origin = "distro" @@ -110,7 +131,9 @@ def test_application_ussuri(status, config, units, model): exp_is_subordinate = False exp_is_valid_track = True - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) assert app.wait_for_model is True assert_application( app, @@ -136,25 +159,27 @@ def test_application_ussuri(status, config, units, model): ) -def test_application_different_wl(status, config, model): +def test_application_different_wl(status, config, model, apps_machines): """Different OpenStack Version on units if workload version is different.""" exp_error_msg = ( "Units of application my_keystone are running mismatched OpenStack versions: " r"'ussuri': \['keystone\/0', 'keystone\/1'\], 'victoria': \['keystone\/2'\]. " "This is not currently handled." ) - app_status = status["keystone_ussuri_victoria"] + app_status = status["keystone_focal_ussuri_victoria"] app_config = config["openstack_ussuri"] - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) with pytest.raises(MismatchedOpenStackVersions, match=exp_error_msg): app.current_os_release -def test_application_cs(status, config, units, model): +def test_application_cs(status, config, units, model, apps_machines): """Test when application is from charm store.""" target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri_cs"] + app_status = status["keystone_focal_ussuri_cs"] app_config = config["openstack_ussuri"] exp_os_origin = "distro" exp_units = units["units_ussuri"] @@ -170,7 +195,9 @@ def test_application_cs(status, config, units, model): exp_is_subordinate = False exp_is_valid_track = True - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) assert_application( app, "my_keystone", @@ -195,12 +222,12 @@ def test_application_cs(status, config, units, model): ) -def test_application_wallaby(status, config, units, model): +def test_application_wallaby(status, config, units, model, apps_machines): target = OpenStackRelease("xena") exp_units = units["units_wallaby"] exp_is_from_charm_store = False app_config = config["openstack_wallaby"] - app_status = status["keystone_wallaby"] + app_status = status["keystone_focal_wallaby"] exp_os_origin = "cloud:focal-wallaby" exp_channel = app_status.charm_channel exp_series = app_status.series @@ -213,7 +240,9 @@ def test_application_wallaby(status, config, units, model): exp_is_subordinate = False exp_is_valid_track = True - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) assert_application( app, "my_keystone", @@ -238,32 +267,34 @@ def test_application_wallaby(status, config, units, model): ) -def test_application_no_origin_config(status, model): +def test_application_no_origin_config(status, model, apps_machines): app = Keystone( "my_keystone", - status["keystone_ussuri"], + status["keystone_focal_ussuri"], {}, model, "keystone", + apps_machines["keystone"], ) assert app._get_os_origin() == "" assert app.apt_source_codename is None -def test_application_empty_origin_config(status, model): +def test_application_empty_origin_config(status, model, apps_machines): app = Keystone( "my_keystone", - status["keystone_ussuri"], + status["keystone_focal_ussuri"], {"source": {"value": ""}}, model, "keystone", + apps_machines["keystone"], ) assert app.apt_source_codename is None -def test_application_unexpected_channel(status, config, model): +def test_application_unexpected_channel(status, config, model, apps_machines): target = OpenStackRelease("xena") - app_status = status["keystone_wallaby"] + app_status = status["keystone_focal_wallaby"] # channel is set to a previous OpenStack release app_status.charm_channel = "ussuri/stable" app = Keystone( @@ -272,6 +303,7 @@ def test_application_unexpected_channel(status, config, model): config["openstack_wallaby"], model, "keystone", + apps_machines["keystone"], ) with pytest.raises(ApplicationError): app.generate_upgrade_plan(target) @@ -281,53 +313,75 @@ def test_application_unexpected_channel(status, config, model): "source_value", ["ppa:myteam/ppa", "cloud:xenial-proposed/ocata", "http://my.archive.com/ubuntu main"], ) -def test_application_unknown_source(status, model, source_value): +def test_application_unknown_source(status, model, source_value, apps_machines): app = Keystone( "my_keystone", - status["keystone_ussuri"], + status["keystone_focal_ussuri"], {"source": {"value": source_value}}, model, "keystone", + apps_machines["keystone"], ) with pytest.raises(ApplicationError): app.apt_source_codename @pytest.mark.asyncio -async def test_application_check_upgrade(status, config, model): +async def test_application_check_upgrade(status, config, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] # workload version changed from ussuri to victoria mock_status = AsyncMock() - mock_status.return_value.applications = {"my_keystone": status["keystone_victoria"]} + mock_status.return_value.applications = {"my_keystone": status["keystone_focal_victoria"]} model.get_status = mock_status - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", + app_status, + app_config, + model, + "keystone", + machines=apps_machines["keystone"], + ) await app._check_upgrade(target) @pytest.mark.asyncio -async def test_application_check_upgrade_fail(status, config, model): +async def test_application_check_upgrade_fail(status, config, model, apps_machines): exp_error_msg = "Cannot upgrade units 'keystone/0, keystone/1, keystone/2' to victoria." target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] # workload version didn't change from ussuri to victoria mock_status = AsyncMock() mock_status.return_value.applications = {"my_keystone": app_status} model.get_status = mock_status - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", + app_status, + app_config, + model, + "keystone", + machines=apps_machines["keystone"], + ) with pytest.raises(ApplicationError, match=exp_error_msg): await app._check_upgrade(target) -def test_upgrade_plan_ussuri_to_victoria(status, config, model): +def test_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", + app_status, + app_config, + model, + "keystone", + machines=apps_machines["keystone"], + ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" @@ -387,11 +441,13 @@ def test_upgrade_plan_ussuri_to_victoria(status, config, model): assert upgrade_plan == expected_plan -def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model): +def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri_cs"] + app_status = status["keystone_focal_ussuri_cs"] app_config = config["openstack_ussuri"] - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" @@ -451,13 +507,15 @@ def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model): assert upgrade_plan == expected_plan -def test_upgrade_plan_channel_on_next_os_release(status, config, model): +def test_upgrade_plan_channel_on_next_os_release(status, config, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] # channel it's already on next OpenStack release app_status.charm_channel = "victoria/stable" - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( @@ -509,13 +567,17 @@ def test_upgrade_plan_channel_on_next_os_release(status, config, model): assert upgrade_plan == expected_plan -def test_upgrade_plan_origin_already_on_next_openstack_release(status, config, model): +def test_upgrade_plan_origin_already_on_next_openstack_release( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] # openstack-origin already configured for next OpenStack release app_config["openstack-origin"]["value"] = "cloud:focal-victoria" - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" @@ -565,31 +627,31 @@ def test_upgrade_plan_origin_already_on_next_openstack_release(status, config, m assert upgrade_plan == expected_plan -def test_upgrade_plan_application_already_upgraded(status, config, model): +def test_upgrade_plan_application_already_upgraded(status, config, model, apps_machines): exp_error_msg = ( "Application 'my_keystone' already configured for release equal or greater " "than victoria. Ignoring." ) target = OpenStackRelease("victoria") - app_status = status["keystone_wallaby"] + app_status = status["keystone_focal_wallaby"] app_config = config["openstack_wallaby"] - app = Keystone("my_keystone", app_status, app_config, model, "keystone") + app = Keystone( + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + ) # victoria is lesser than wallaby, so application should not generate a plan. with pytest.raises(HaltUpgradePlanGeneration, match=exp_error_msg): app.generate_upgrade_plan(target) -def test_upgrade_plan_application_already_disable_action_managed(status, config, model): +def test_upgrade_plan_application_already_disable_action_managed( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") - app_status = status["keystone_ussuri"] + app_status = status["keystone_focal_ussuri"] app_config = config["openstack_ussuri"] app_config["action-managed-upgrade"]["value"] = False app = Keystone( - "my_keystone", - app_status, - app_config, - model, - "keystone", + "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( diff --git a/tests/unit/apps/test_machines.py b/tests/unit/apps/test_machines.py new file mode 100644 index 00000000..8d36e6b6 --- /dev/null +++ b/tests/unit/apps/test_machines.py @@ -0,0 +1,56 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from cou.apps.machine import Machine + + +@pytest.mark.parametrize( + "machine_id, hostname, az, is_data_plane", + [ + # one field different is considered another machine + ("0", "juju-c307f8-my_model-0", "zone-1", True), + ("1", "juju-c307f8-my_model-1", "zone-2", False), + ], +) +def test_machine_not_eq(machine_id, hostname, az, is_data_plane): + machine_0 = Machine( + machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False + ) + + machine_1 = Machine( + machine_id=machine_id, hostname=hostname, az=az, is_data_plane=is_data_plane + ) + + assert machine_0 != machine_1 + + +def test_machine_eq(): + machine_0 = Machine( + machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False + ) + + machine_1 = Machine( + machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False + ) + + assert machine_0 == machine_1 + + +def test_machine_repr(): + machine_0 = Machine( + machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False + ) + expected_repr = "Machine[0]" + assert repr(machine_0) == expected_repr diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index cf9b71dc..158d0465 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -24,380 +24,437 @@ from cou.apps.auxiliary_subordinate import OpenStackAuxiliarySubordinateApplication from cou.apps.base import ApplicationUnit, OpenStackApplication from cou.apps.core import Keystone +from cou.apps.machine import Machine from cou.apps.subordinate import OpenStackSubordinateApplication from cou.utils.openstack import OpenStackRelease +STANDARD_AZS = ["zone-1", "zone-2", "zone-3"] +HOSTNAME_PREFIX = "juju-c307f8" -def generate_unit(workload_version, machine): +KEYSTONE_UNITS = ["keystone/0", "keystone/1", "keystone/2"] +KEYSTONE_MACHINES = ["0/lxd/12", "1/lxd/12", "2/lxd/13"] +KEYSTONE_WORKLOADS = { + "ussuri": ["17.0.1", "17.0.1", "17.0.1"], + "victoria": ["18.1.0", "18.1.0", "18.1.0"], + "ussuri-victoria": ["17.0.1", "17.0.1", "18.1.0"], + "wallaby": ["19.1.0", "19.1.0", "19.1.0"], +} + + +def _generate_unit(workload_version, machine): unit = MagicMock(spec_set=UnitStatus()) unit.workload_version = workload_version unit.machine = machine return unit -@pytest.fixture -def status(): - mock_keystone_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_ussuri.series = "focal" - mock_keystone_ussuri.charm_channel = "ussuri/stable" - mock_keystone_ussuri.charm = "ch:amd64/focal/keystone-638" - mock_keystone_ussuri.subordinate_to = [] - mock_keystone_ussuri.units = OrderedDict( - [ - ("keystone/0", generate_unit("17.0.1", "0/lxd/12")), - ("keystone/1", generate_unit("17.0.1", "1/lxd/12")), - ("keystone/2", generate_unit("17.0.1", "2/lxd/13")), - ] - ) +def _generate_units(units_machines_workloads): + unit = MagicMock(spec_set=UnitStatus()) - mock_keystone_bionic_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_bionic_ussuri.series = "bionic" - mock_keystone_bionic_ussuri.charm_channel = "ussuri/stable" - mock_keystone_bionic_ussuri.charm = "ch:amd64/bionic/keystone-638" - mock_keystone_bionic_ussuri.subordinate_to = [] - mock_keystone_bionic_ussuri.units = OrderedDict( - [ - ("keystone/0", generate_unit("17.0.1", "0/lxd/12")), - ("keystone/1", generate_unit("17.0.1", "1/lxd/12")), - ("keystone/2", generate_unit("17.0.1", "2/lxd/13")), - ] - ) + ordered_units = OrderedDict() + for unit_machine_workload in units_machines_workloads: + unit, machine, workload = unit_machine_workload + ordered_units[unit] = _generate_unit(workload, machine) - mock_cinder_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_cinder_ussuri.series = "focal" - mock_cinder_ussuri.charm_channel = "ussuri/stable" - mock_cinder_ussuri.charm = "ch:amd64/focal/cinder-633" - mock_cinder_ussuri.subordinate_to = [] - mock_cinder_ussuri.units = OrderedDict( - [ - ("cinder/0", generate_unit("16.4.2", "0/lxd/5")), - ("cinder/1", generate_unit("16.4.2", "1/lxd/5")), - ("cinder/2", generate_unit("16.4.2", "2/lxd/5")), - ] - ) + return ordered_units - mock_cinder_on_nova = MagicMock(spec_set=ApplicationStatus()) - mock_cinder_on_nova.series = "focal" - mock_cinder_on_nova.charm_channel = "ussuri/stable" - mock_cinder_on_nova.charm = "ch:amd64/focal/cinder-633" - mock_cinder_on_nova.subordinate_to = [] - mock_cinder_on_nova.units = OrderedDict( - [ - ("cinder/0", generate_unit("16.4.2", "0")), - ("cinder/1", generate_unit("16.4.2", "1")), - ("cinder/2", generate_unit("16.4.2", "2")), - ] - ) - mock_keystone_ussuri_cs = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_ussuri_cs.series = "focal" - mock_keystone_ussuri_cs.charm_channel = "stable" - mock_keystone_ussuri_cs.charm = "cs:amd64/focal/keystone-638" - mock_keystone_ussuri_cs.subordinate_to = [] - mock_keystone_ussuri_cs.units = OrderedDict( - [ - ("keystone/0", generate_unit("17.0.1", "0/lxd/12")), - ("keystone/1", generate_unit("17.0.1", "1/lxd/12")), - ("keystone/2", generate_unit("17.0.1", "2/lxd/13")), - ] - ) - - mock_keystone_victoria = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_victoria.series = "focal" - mock_keystone_victoria.charm_channel = "wallaby/stable" - mock_keystone_victoria.charm = "ch:amd64/focal/keystone-638" - mock_keystone_victoria.subordinate_to = [] - mock_keystone_victoria.units = OrderedDict( - [ - ("keystone/0", generate_unit("18.1.0", "0/lxd/12")), - ("keystone/1", generate_unit("18.1.0", "1/lxd/12")), - ("keystone/2", generate_unit("18.1.0", "2/lxd/13")), - ] - ) - - mock_keystone_ussuri_victoria = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_ussuri_victoria.series = "focal" - mock_keystone_ussuri_victoria.charm_channel = "victoria/stable" - mock_keystone_ussuri_victoria.charm = "ch:amd64/focal/keystone-638" - mock_keystone_ussuri_victoria.subordinate_to = [] - mock_keystone_ussuri_victoria.units = OrderedDict( - [ - ("keystone/0", generate_unit("17.0.1", "0/lxd/12")), - ("keystone/1", generate_unit("17.0.1", "1/lxd/12")), - ("keystone/2", generate_unit("18.1.0", "2/lxd/13")), - ] - ) +@pytest.fixture +def apps_machines(): + return { + **_generate_apps_machines( + "keystone", KEYSTONE_MACHINES, STANDARD_AZS, [False, False, False] + ), + } - mock_keystone_wallaby = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_wallaby.series = "focal" - mock_keystone_wallaby.charm_channel = "wallaby/stable" - mock_keystone_wallaby.charm = "ch:amd64/focal/keystone-638" - mock_keystone_wallaby.subordinate_to = [] - mock_keystone_wallaby.units = OrderedDict( - [ - ("keystone/0", generate_unit("19.1.0", "0/lxd/12")), - ("keystone/1", generate_unit("19.1.0", "1/lxd/12")), - ("keystone/2", generate_unit("19.1.0", "2/lxd/13")), - ] - ) - # gnocchi on ussuri - mock_gnocchi_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_gnocchi_ussuri.series = "focal" - mock_gnocchi_ussuri.charm_channel = "ussuri/stable" - mock_gnocchi_ussuri.charm = "ch:amd64/focal/gnocchi-638" - mock_gnocchi_ussuri.subordinate_to = [] - mock_gnocchi_ussuri.units = OrderedDict( - [ - ("gnocchi/0", generate_unit("4.3.4", "3/lxd/6")), - ("gnocchi/1", generate_unit("4.3.4", "4/lxd/6")), - ("gnocchi/2", generate_unit("4.3.4", "5/lxd/5")), - ] - ) +def _generate_apps_machines(charm, machines, azs, is_data_plane): + hostnames = [f"{HOSTNAME_PREFIX}-{machine}" for machine in machines] + machines_hostnames_azs_is_data_plane = zip(machines, hostnames, azs, is_data_plane) + return { + charm: { + machine_id: Machine( + machine_id=machine_id, hostname=hostname, az=az, is_data_plane=is_data_plane + ) + for machine_id, hostname, az, is_data_plane in machines_hostnames_azs_is_data_plane + } + } - # gnocchi on xena - mock_gnocchi_xena = MagicMock(spec_set=ApplicationStatus()) - mock_gnocchi_xena.series = "focal" - mock_gnocchi_xena.charm_channel = "xena/stable" - mock_gnocchi_xena.charm = "ch:amd64/focal/gnocchi-638" - mock_gnocchi_xena.subordinate_to = [] - mock_gnocchi_xena.units = OrderedDict( - [ - ("gnocchi/0", generate_unit("4.4.1", "3/lxd/6")), - ("gnocchi/1", generate_unit("4.4.1", "4/lxd/6")), - ("gnocchi/2", generate_unit("4.4.1", "5/lxd/5")), - ] - ) - # designate-bind on ussuri - mock_designate_bind_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_designate_bind_ussuri.series = "focal" - mock_designate_bind_ussuri.charm_channel = "ussuri/stable" - mock_designate_bind_ussuri.charm = "ch:amd64/focal/designate-bind-737" - mock_designate_bind_ussuri.subordinate_to = [] - mock_designate_bind_ussuri.units = OrderedDict( - [ - ("gnocchi/0", generate_unit("9.16.1", "1/lxd/6")), - ("gnocchi/1", generate_unit("9.16.1", "2/lxd/6")), - ] - ) +@pytest.fixture +def status(): + # mock_cinder_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_cinder_ussuri.series = "focal" + # mock_cinder_ussuri.charm_channel = "ussuri/stable" + # mock_cinder_ussuri.charm = "ch:amd64/focal/cinder-633" + # mock_cinder_ussuri.subordinate_to = [] + # mock_cinder_ussuri.units = OrderedDict( + # [ + # ("cinder/0", generate_unit("cinder", "16.4.2", "0/lxd/5")), + # ("cinder/1", generate_unit("cinder", "16.4.2", "1/lxd/5")), + # ("cinder/2", generate_unit("cinder", "16.4.2", "2/lxd/5")), + # ] + # ) + + # mock_cinder_on_nova = MagicMock(spec_set=ApplicationStatus()) + # mock_cinder_on_nova.series = "focal" + # mock_cinder_on_nova.charm_channel = "ussuri/stable" + # mock_cinder_on_nova.charm = "ch:amd64/focal/cinder-633" + # mock_cinder_on_nova.subordinate_to = [] + # mock_cinder_on_nova.units = OrderedDict( + # [ + # ("cinder/0", generate_unit("cinder-nova", "16.4.2", "0")), + # ("cinder/1", generate_unit("cinder-nova", "16.4.2", "1")), + # ("cinder/2", generate_unit("cinder-nova", "16.4.2", "2")), + # ] + # ) + + # # gnocchi on ussuri + # mock_gnocchi_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_gnocchi_ussuri.series = "focal" + # mock_gnocchi_ussuri.charm_channel = "ussuri/stable" + # mock_gnocchi_ussuri.charm = "ch:amd64/focal/gnocchi-638" + # mock_gnocchi_ussuri.subordinate_to = [] + # mock_gnocchi_ussuri.units = OrderedDict( + # [ + # ("gnocchi/0", generate_unit("gnocchi", "4.3.4", "3/lxd/6")), + # ("gnocchi/1", generate_unit("gnocchi", "4.3.4", "4/lxd/6")), + # ("gnocchi/2", generate_unit("gnocchi", "4.3.4", "5/lxd/5")), + # ] + # ) + + # # gnocchi on xena + # mock_gnocchi_xena = MagicMock(spec_set=ApplicationStatus()) + # mock_gnocchi_xena.series = "focal" + # mock_gnocchi_xena.charm_channel = "xena/stable" + # mock_gnocchi_xena.charm = "ch:amd64/focal/gnocchi-638" + # mock_gnocchi_xena.subordinate_to = [] + # mock_gnocchi_xena.units = OrderedDict( + # [ + # ("gnocchi/0", generate_unit("gnocchi", "4.4.1", "3/lxd/6")), + # ("gnocchi/1", generate_unit("gnocchi", "4.4.1", "4/lxd/6")), + # ("gnocchi/2", generate_unit("gnocchi", "4.4.1", "5/lxd/5")), + # ] + # ) + + # # designate-bind on ussuri + # mock_designate_bind_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_designate_bind_ussuri.series = "focal" + # mock_designate_bind_ussuri.charm_channel = "ussuri/stable" + # mock_designate_bind_ussuri.charm = "ch:amd64/focal/designate-bind-737" + # mock_designate_bind_ussuri.subordinate_to = [] + # mock_designate_bind_ussuri.units = OrderedDict( + # [ + # ("designate-bind/0", generate_unit("designate-bind", "9.16.1", "1/lxd/6")), + # ("designate-bind/1", generate_unit("designate-bind","9.16.1", "2/lxd/6")), + # ] + # ) + + # mock_nova_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_nova_ussuri.series = "focal" + # mock_nova_ussuri.charm_channel = "ussuri/stable" + # mock_nova_ussuri.charm = "ch:amd64/focal/nova-compute-638" + # mock_nova_ussuri.subordinate_to = [] + # mock_nova_ussuri.units = OrderedDict( + # [ + # ("nova-compute/0", generate_unit("nova-compute", "21.0.0", "0")), + # ("nova-compute/1", generate_unit("nova-compute", "21.0.0", "1")), + # ("nova-compute/2", generate_unit("nova-compute", "21.0.0", "2")), + # ] + # ) + + # mock_nova_wallaby = MagicMock(spec_set=ApplicationStatus()) + # mock_nova_wallaby.series = "focal" + # mock_nova_wallaby.charm_channel = "wallaby/stable" + # mock_nova_wallaby.charm = "ch:amd64/focal/nova-compute-638" + # mock_nova_wallaby.subordinate_to = [] + # mock_nova_wallaby.units = OrderedDict( + # [ + # ("nova-compute/0", generate_unit("nova-compute", "24.1.0", "0")), + # ("nova-compute/1", generate_unit("nova-compute", "24.1.0", "1")), + # ("nova-compute/2", generate_unit("nova-compute", "24.1.0", "2")), + # ] + # ) + + # # glance-simplestreams-sync does not have workload_version + # mock_glance_simplestreams_sync_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_glance_simplestreams_sync_ussuri.series = "focal" + # mock_glance_simplestreams_sync_ussuri.charm_channel = "ussuri/stable" + # mock_glance_simplestreams_sync_ussuri.charm = "ch:amd64/focal/glance-simplestreams-sync-78" + # mock_glance_simplestreams_sync_ussuri.subordinate_to = [] + # mock_glance_simplestreams_sync_ussuri.units = OrderedDict( + # [ + # ("glance-simplestreams-sync/0", generate_unit("glance-simplestreams-sync", "", "4/lxd/5")), + # ] + # ) + + # mock_rmq = MagicMock(spec_set=ApplicationStatus()) + # mock_rmq.series = "focal" + # mock_rmq.charm_channel = "3.8/stable" + # mock_rmq.charm = "ch:amd64/focal/rabbitmq-server-638" + # mock_rmq.subordinate_to = [] + # mock_rmq.units = OrderedDict([("rabbitmq-server/0", generate_unit("rabbitmq-server", "3.8", "0/lxd/19"))]) + + # mock_rmq_unknown = MagicMock(spec_set=ApplicationStatus()) + # mock_rmq_unknown.series = "focal" + # mock_rmq_unknown.charm_channel = "80.5/stable" + # mock_rmq_unknown.charm = "ch:amd64/focal/rabbitmq-server-638" + # mock_rmq_unknown.subordinate_to = [] + # mock_rmq_unknown.units = OrderedDict( + # [("rabbitmq-server/0", generate_unit("rabbitmq-server", "80.5", "0/lxd/19"))] + # ) + + # mock_unknown_app = MagicMock(spec_set=ApplicationStatus()) + # mock_unknown_app.series = "focal" + # mock_unknown_app.charm_channel = "12.5/stable" + # mock_unknown_app.charm = "ch:amd64/focal/my-app-638" + # mock_unknown_app.subordinate_to = [] + # mock_unknown_app.units = OrderedDict([("my-app/0", generate_unit("my-app", "12.5", "0/lxd/11"))]) + + # # openstack related principal application without openstack origin or source + # mock_vault = MagicMock(spec_set=ApplicationStatus()) + # mock_vault.series = "focal" + # mock_vault.charm_channel = "1.7/stable" + # mock_vault.charm = "ch:amd64/focal/vault-638" + # mock_vault.subordinate_to = [] + # mock_vault.units = OrderedDict([("vault/0", generate_unit("vault", "1.7", "5"))]) + + # # auxiliary subordinate application + # mock_mysql_router = MagicMock(spec_set=ApplicationStatus()) + # mock_mysql_router.series = "focal" + # mock_mysql_router.charm_channel = "8.0/stable" + # mock_mysql_router.charm = "ch:amd64/focal/mysql-router-437" + # mock_mysql_router.subordinate_to = ["keystone"] + # mock_mysql_router.units = {} + + # # OpenStack subordinate application + # mock_keystone_ldap = MagicMock(spec_set=ApplicationStatus()) + # mock_keystone_ldap.series = "focal" + # mock_keystone_ldap.charm_channel = "ussuri/stable" + # mock_keystone_ldap.charm = "ch:amd64/focal/keystone-ldap-437" + # mock_keystone_ldap.subordinate_to = ["keystone"] + # mock_keystone_ldap.units = {} + + # # OpenStack subordinate application cs + # mock_keystone_ldap_cs = MagicMock(spec_set=ApplicationStatus()) + # mock_keystone_ldap_cs.series = "focal" + # mock_keystone_ldap_cs.charm_channel = "stable" + # mock_keystone_ldap_cs.charm = "cs:amd64/focal/keystone-ldap-437" + # mock_keystone_ldap_cs.subordinate_to = ["keystone"] + # mock_keystone_ldap_cs.units = {} + + # # ceph-mon application on ussuri + # mock_ceph_mon_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_ceph_mon_ussuri.series = "focal" + # mock_ceph_mon_ussuri.charm_channel = "octopus/stable" + # mock_ceph_mon_ussuri.charm = "ch:amd64/focal/ceph-mon-177" + # mock_ceph_mon_ussuri.subordinate_to = [] + # mock_ceph_mon_ussuri.units = OrderedDict([("ceph-mon/0", generate_unit("ceph-mon", "15.2.0", "6"))]) + + # # ceph-mon application on xena + # mock_ceph_mon_xena = MagicMock(spec_set=ApplicationStatus()) + # mock_ceph_mon_xena.series = "focal" + # mock_ceph_mon_xena.charm_channel = "pacific/stable" + # mock_ceph_mon_xena.charm = "ch:amd64/focal/ceph-mon-178" + # mock_ceph_mon_xena.subordinate_to = [] + # mock_ceph_mon_xena.units = OrderedDict([("ceph-mon/0", generate_unit("ceph-mon", "16.2.0", "7"))]) + + # # ceph-osd application on ussuri + # mock_ceph_osd_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_ceph_osd_ussuri.series = "focal" + # mock_ceph_osd_ussuri.charm_channel = "octopus/stable" + # mock_ceph_osd_ussuri.charm = "ch:amd64/focal/ceph-osd-177" + # mock_ceph_osd_ussuri.subordinate_to = [] + # mock_ceph_osd_ussuri.units = OrderedDict([("ceph-osd/0", generate_unit("ceph-mon", "15.2.0", "6"))]) + + # # mysql-innodb-cluster application on ussuri using 8.0 + # mock_mysql_innodb_cluster_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_mysql_innodb_cluster_ussuri.series = "focal" + # mock_mysql_innodb_cluster_ussuri.charm_channel = "8.0/stable" + # mock_mysql_innodb_cluster_ussuri.charm = "ch:amd64/focal/mysql-innodb-cluster-106" + # mock_mysql_innodb_cluster_ussuri.subordinate_to = [] + # mock_mysql_innodb_cluster_ussuri.units = OrderedDict( + # [("ovn-central/0", generate_unit("ovn-central", "8.0", "0/lxd/7"))] + # ) + + # # ovn-central application on ussuri using 22.03 + # mock_ovn_central_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) + # mock_ovn_central_ussuri_22.series = "focal" + # mock_ovn_central_ussuri_22.charm_channel = "22.03/stable" + # mock_ovn_central_ussuri_22.charm = "ch:amd64/focal/ovn-central-178" + # mock_ovn_central_ussuri_22.subordinate_to = [] + # mock_ovn_central_ussuri_22.units = OrderedDict( + # [("ovn-central/0", generate_unit("ovn-central", "22.03.2", "0/lxd/7"))] + # ) + + # # ovn-central application on ussuri using 20.03 + # mock_ovn_central_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) + # mock_ovn_central_ussuri_20.series = "focal" + # mock_ovn_central_ussuri_20.charm_channel = "20.03/stable" + # mock_ovn_central_ussuri_20.charm = "ch:amd64/focal/ovn-central-178" + # mock_ovn_central_ussuri_20.subordinate_to = [] + # mock_ovn_central_ussuri_20.units = OrderedDict( + # [("ovn-central/0", generate_unit("ovn-central", "20.03.2", "0/lxd/7"))] + # ) + + # # ovn-chassis application on ussuri using 22.03 + # mock_ovn_chassis_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) + # mock_ovn_chassis_ussuri_22.series = "focal" + # mock_ovn_chassis_ussuri_22.charm_channel = "22.03/stable" + # mock_ovn_chassis_ussuri_22.charm = "ch:amd64/focal/ovn-chassis-178" + # mock_ovn_chassis_ussuri_22.workload_version = "22.03.2" + # mock_ovn_chassis_ussuri_22.subordinate_to = ["nova-compute"] + # mock_ovn_chassis_ussuri_22.units = {} + + # # ovn-chassis application on ussuri using 20.03 + # mock_ovn_chassis_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) + # mock_ovn_chassis_ussuri_20.series = "focal" + # mock_ovn_chassis_ussuri_20.charm_channel = "20.03/stable" + # mock_ovn_chassis_ussuri_20.charm = "ch:amd64/focal/ovn-chassis-178" + # mock_ovn_chassis_ussuri_20.subordinate_to = ["nova-compute"] + # mock_ovn_chassis_ussuri_20.workload_version = "20.03.2" + # mock_ovn_chassis_ussuri_20.units = {} + + # # ceph-dashboard on ussuri + # mock_ceph_dashboard_ussuri = MagicMock(spec_set=ApplicationStatus()) + # mock_ceph_dashboard_ussuri.series = "focal" + # mock_ceph_dashboard_ussuri.charm_channel = "octopus/stable" + # mock_ceph_dashboard_ussuri.charm = "ch:amd64/focal/ceph-dashboard-178" + # mock_ceph_dashboard_ussuri.subordinate_to = ["ceph-mon"] + # mock_ceph_dashboard_ussuri.units = {} + + # # ceph-dashboard on xena + # mock_ceph_dashboard_xena = MagicMock(spec_set=ApplicationStatus()) + # mock_ceph_dashboard_xena.series = "focal" + # mock_ceph_dashboard_xena.charm_channel = "pacific/stable" + # mock_ceph_dashboard_xena.charm = "ch:amd64/focal/ceph-dashboard-178" + # mock_ceph_dashboard_xena.subordinate_to = ["ceph-mon"] + # mock_ceph_dashboard_xena.units = {} - mock_nova_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_nova_ussuri.series = "focal" - mock_nova_ussuri.charm_channel = "ussuri/stable" - mock_nova_ussuri.charm = "ch:amd64/focal/nova-compute-638" - mock_nova_ussuri.subordinate_to = [] - mock_nova_ussuri.units = OrderedDict( - [ - ("nova-compute/0", generate_unit("21.0.0", "0")), - ("nova-compute/1", generate_unit("21.0.0", "1")), - ("nova-compute/2", generate_unit("21.0.0", "2")), - ] - ) + status = { + **generate_keystone_status() + # "cinder_ussuri": mock_cinder_ussuri, + # "glance_simplestreams_sync_ussuri": mock_glance_simplestreams_sync_ussuri, + # "gnocchi_ussuri": mock_gnocchi_ussuri, + # "gnocchi_xena": mock_gnocchi_xena, + # "designate_bind_ussuri": mock_designate_bind_ussuri, + # "rabbitmq_server": mock_rmq, + # "unknown_rabbitmq_server": mock_rmq_unknown, + # "unknown_app": mock_unknown_app, + # "mysql_router": mock_mysql_router, + # "vault": mock_vault, + # "keystone-ldap": mock_keystone_ldap, + # "keystone-ldap-cs": mock_keystone_ldap_cs, + # "nova_ussuri": mock_nova_ussuri, + # "nova_wallaby": mock_nova_wallaby, + # "ceph-mon_ussuri": mock_ceph_mon_ussuri, + # "ceph-mon_xena": mock_ceph_mon_xena, + # "ceph_osd_ussuri": mock_ceph_osd_ussuri, + # "ceph_dashboard_ussuri": mock_ceph_dashboard_ussuri, + # "ceph_dashboard_xena": mock_ceph_dashboard_xena, + # "cinder_ussuri_on_nova": mock_cinder_on_nova, + # "mysql-innodb-cluster": mock_mysql_innodb_cluster_ussuri, + # "ovn_central_ussuri_22": mock_ovn_central_ussuri_22, + # "ovn_central_ussuri_20": mock_ovn_central_ussuri_20, + # "ovn_chassis_ussuri_22": mock_ovn_chassis_ussuri_22, + # "ovn_chassis_ussuri_20": mock_ovn_chassis_ussuri_20, + } + return status - mock_nova_wallaby = MagicMock(spec_set=ApplicationStatus()) - mock_nova_wallaby.series = "focal" - mock_nova_wallaby.charm_channel = "wallaby/stable" - mock_nova_wallaby.charm = "ch:amd64/focal/nova-compute-638" - mock_nova_wallaby.subordinate_to = [] - mock_nova_wallaby.units = OrderedDict( - [ - ("nova-compute/0", generate_unit("24.1.0", "0")), - ("nova-compute/1", generate_unit("24.1.0", "1")), - ("nova-compute/2", generate_unit("24.1.0", "2")), - ] - ) - # glance-simplestreams-sync does not have workload_version - mock_glance_simplestreams_sync_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_glance_simplestreams_sync_ussuri.series = "focal" - mock_glance_simplestreams_sync_ussuri.charm_channel = "ussuri/stable" - mock_glance_simplestreams_sync_ussuri.charm = "ch:amd64/focal/glance-simplestreams-sync-78" - mock_glance_simplestreams_sync_ussuri.subordinate_to = [] - mock_glance_simplestreams_sync_ussuri.units = OrderedDict( - [ - ("glance-simplestreams-sync/0", generate_unit("", "4/lxd/5")), - ] - ) +def generate_keystone_status(): + keystone_units = ["keystone/0", "keystone/1", "keystone/2"] + keystone_machines = ["0/lxd/12", "1/lxd/12", "2/lxd/13"] - mock_rmq = MagicMock(spec_set=ApplicationStatus()) - mock_rmq.series = "focal" - mock_rmq.charm_channel = "3.8/stable" - mock_rmq.charm = "ch:amd64/focal/rabbitmq-server-638" - mock_rmq.subordinate_to = [] - mock_rmq.units = OrderedDict([("rabbitmq-server/0", generate_unit("3.8", "0/lxd/19"))]) - - mock_rmq_unknown = MagicMock(spec_set=ApplicationStatus()) - mock_rmq_unknown.series = "focal" - mock_rmq_unknown.charm_channel = "80.5/stable" - mock_rmq_unknown.charm = "ch:amd64/focal/rabbitmq-server-638" - mock_rmq_unknown.subordinate_to = [] - mock_rmq_unknown.units = OrderedDict( - [("rabbitmq-server/0", generate_unit("80.5", "0/lxd/19"))] - ) + keystone_workloads = { + "ussuri": ["17.0.1", "17.0.1", "17.0.1"], + "victoria": ["18.1.0", "18.1.0", "18.1.0"], + "ussuri-victoria": ["17.0.1", "17.0.1", "18.1.0"], + "wallaby": ["19.1.0", "19.1.0", "19.1.0"], + } - mock_unknown_app = MagicMock(spec_set=ApplicationStatus()) - mock_unknown_app.series = "focal" - mock_unknown_app.charm_channel = "12.5/stable" - mock_unknown_app.charm = "ch:amd64/focal/my-app-638" - mock_unknown_app.subordinate_to = [] - mock_unknown_app.units = OrderedDict([("my-app/0", generate_unit("12.5", "0/lxd/11"))]) - - # openstack related principal application without openstack origin or source - mock_vault = MagicMock(spec_set=ApplicationStatus()) - mock_vault.series = "focal" - mock_vault.charm_channel = "1.7/stable" - mock_vault.charm = "ch:amd64/focal/vault-638" - mock_vault.subordinate_to = [] - mock_vault.units = OrderedDict([("vault/0", generate_unit("1.7", "5"))]) - - # auxiliary subordinate application - mock_mysql_router = MagicMock(spec_set=ApplicationStatus()) - mock_mysql_router.series = "focal" - mock_mysql_router.charm_channel = "8.0/stable" - mock_mysql_router.charm = "ch:amd64/focal/mysql-router-437" - mock_mysql_router.subordinate_to = ["keystone"] - mock_mysql_router.units = {} - - # OpenStack subordinate application - mock_keystone_ldap = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_ldap.series = "focal" - mock_keystone_ldap.charm_channel = "ussuri/stable" - mock_keystone_ldap.charm = "ch:amd64/focal/keystone-ldap-437" - mock_keystone_ldap.subordinate_to = ["keystone"] - mock_keystone_ldap.units = {} - - # OpenStack subordinate application cs - mock_keystone_ldap_cs = MagicMock(spec_set=ApplicationStatus()) - mock_keystone_ldap_cs.series = "focal" - mock_keystone_ldap_cs.charm_channel = "stable" - mock_keystone_ldap_cs.charm = "cs:amd64/focal/keystone-ldap-437" - mock_keystone_ldap_cs.subordinate_to = ["keystone"] - mock_keystone_ldap_cs.units = {} - - # ceph-mon application on ussuri - mock_ceph_mon_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_ceph_mon_ussuri.series = "focal" - mock_ceph_mon_ussuri.charm_channel = "octopus/stable" - mock_ceph_mon_ussuri.charm = "ch:amd64/focal/ceph-mon-177" - mock_ceph_mon_ussuri.subordinate_to = [] - mock_ceph_mon_ussuri.units = OrderedDict([("ceph-mon/0", generate_unit("15.2.0", "6"))]) - - # ceph-mon application on xena - mock_ceph_mon_xena = MagicMock(spec_set=ApplicationStatus()) - mock_ceph_mon_xena.series = "focal" - mock_ceph_mon_xena.charm_channel = "pacific/stable" - mock_ceph_mon_xena.charm = "ch:amd64/focal/ceph-mon-178" - mock_ceph_mon_xena.subordinate_to = [] - mock_ceph_mon_xena.units = OrderedDict([("ceph-mon/0", generate_unit("16.2.0", "7"))]) - - # ceph-osd application on ussuri - mock_ceph_osd_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_ceph_osd_ussuri.series = "focal" - mock_ceph_osd_ussuri.charm_channel = "octopus/stable" - mock_ceph_osd_ussuri.charm = "ch:amd64/focal/ceph-osd-177" - mock_ceph_osd_ussuri.subordinate_to = [] - mock_ceph_osd_ussuri.units = OrderedDict([("ceph-osd/0", generate_unit("15.2.0", "6"))]) - - # mysql-innodb-cluster application on ussuri using 8.0 - mock_mysql_innodb_cluster_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_mysql_innodb_cluster_ussuri.series = "focal" - mock_mysql_innodb_cluster_ussuri.charm_channel = "8.0/stable" - mock_mysql_innodb_cluster_ussuri.charm = "ch:amd64/focal/mysql-innodb-cluster-106" - mock_mysql_innodb_cluster_ussuri.subordinate_to = [] - mock_mysql_innodb_cluster_ussuri.units = OrderedDict( - [("ovn-central/0", generate_unit("8.0", "0/lxd/7"))] + mock_keystone_focal_ussuri = _generate_status( + "focal", + "ussuri/stable", + "ch:amd64/focal/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["ussuri"], + ) + + mock_keystone_bionic_ussuri = _generate_status( + "bionic", + "ussuri/stable", + "ch:amd64/bionic/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["ussuri"], + ) + + mock_keystone_focal_ussuri_cs = _generate_status( + "focal", + "ussuri/stable", + "cs:amd64/focal/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["ussuri"], + ) + + mock_keystone_focal_victoria = _generate_status( + "focal", + "wallaby/stable", + "ch:amd64/focal/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["victoria"], + ) + + mock_keystone_focal_ussuri_victoria = _generate_status( + "focal", + "victoria/stable", + "ch:amd64/focal/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["ussuri-victoria"], + ) + + mock_keystone_focal_wallaby = _generate_status( + "focal", + "wallaby/stable", + "ch:amd64/focal/keystone-638", + [], + keystone_units, + keystone_machines, + keystone_workloads["wallaby"], ) - # ovn-central application on ussuri using 22.03 - mock_ovn_central_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) - mock_ovn_central_ussuri_22.series = "focal" - mock_ovn_central_ussuri_22.charm_channel = "22.03/stable" - mock_ovn_central_ussuri_22.charm = "ch:amd64/focal/ovn-central-178" - mock_ovn_central_ussuri_22.subordinate_to = [] - mock_ovn_central_ussuri_22.units = OrderedDict( - [("ovn-central/0", generate_unit("22.03.2", "0/lxd/7"))] - ) + return { + "keystone_focal_ussuri": mock_keystone_focal_ussuri, + "keystone_bionic_ussuri": mock_keystone_bionic_ussuri, + "keystone_focal_ussuri_cs": mock_keystone_focal_ussuri_cs, + "keystone_focal_ussuri_victoria": mock_keystone_focal_ussuri_victoria, + "keystone_focal_victoria": mock_keystone_focal_victoria, + "keystone_focal_wallaby": mock_keystone_focal_wallaby, + } - # ovn-central application on ussuri using 20.03 - mock_ovn_central_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) - mock_ovn_central_ussuri_20.series = "focal" - mock_ovn_central_ussuri_20.charm_channel = "20.03/stable" - mock_ovn_central_ussuri_20.charm = "ch:amd64/focal/ovn-central-178" - mock_ovn_central_ussuri_20.subordinate_to = [] - mock_ovn_central_ussuri_20.units = OrderedDict( - [("ovn-central/0", generate_unit("20.03.2", "0/lxd/7"))] - ) - # ovn-chassis application on ussuri using 22.03 - mock_ovn_chassis_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) - mock_ovn_chassis_ussuri_22.series = "focal" - mock_ovn_chassis_ussuri_22.charm_channel = "22.03/stable" - mock_ovn_chassis_ussuri_22.charm = "ch:amd64/focal/ovn-chassis-178" - mock_ovn_chassis_ussuri_22.workload_version = "22.03.2" - mock_ovn_chassis_ussuri_22.subordinate_to = ["nova-compute"] - mock_ovn_chassis_ussuri_22.units = {} - - # ovn-chassis application on ussuri using 20.03 - mock_ovn_chassis_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) - mock_ovn_chassis_ussuri_20.series = "focal" - mock_ovn_chassis_ussuri_20.charm_channel = "20.03/stable" - mock_ovn_chassis_ussuri_20.charm = "ch:amd64/focal/ovn-chassis-178" - mock_ovn_chassis_ussuri_20.subordinate_to = ["nova-compute"] - mock_ovn_chassis_ussuri_20.workload_version = "20.03.2" - mock_ovn_chassis_ussuri_20.units = {} - - # ceph-dashboard on ussuri - mock_ceph_dashboard_ussuri = MagicMock(spec_set=ApplicationStatus()) - mock_ceph_dashboard_ussuri.series = "focal" - mock_ceph_dashboard_ussuri.charm_channel = "octopus/stable" - mock_ceph_dashboard_ussuri.charm = "ch:amd64/focal/ceph-dashboard-178" - mock_ceph_dashboard_ussuri.subordinate_to = ["ceph-mon"] - mock_ceph_dashboard_ussuri.units = {} - - # ceph-dashboard on xena - mock_ceph_dashboard_xena = MagicMock(spec_set=ApplicationStatus()) - mock_ceph_dashboard_xena.series = "focal" - mock_ceph_dashboard_xena.charm_channel = "pacific/stable" - mock_ceph_dashboard_xena.charm = "ch:amd64/focal/ceph-dashboard-178" - mock_ceph_dashboard_xena.subordinate_to = ["ceph-mon"] - mock_ceph_dashboard_xena.units = {} +def _generate_status( + series, charm_channel, charm, subordinate_to, units, machines, workload_versions +): + app_mock = MagicMock(spec_set=ApplicationStatus()) + app_mock.series = series + app_mock.charm_channel = charm_channel + app_mock.charm = charm + app_mock.subordinate_to = subordinate_to - status = { - "keystone_ussuri": mock_keystone_ussuri, - "keystone_victoria": mock_keystone_victoria, - "keystone_wallaby": mock_keystone_wallaby, - "keystone_ussuri_victoria": mock_keystone_ussuri_victoria, - "keystone_bionic_ussuri": mock_keystone_bionic_ussuri, - "cinder_ussuri": mock_cinder_ussuri, - "glance_simplestreams_sync_ussuri": mock_glance_simplestreams_sync_ussuri, - "gnocchi_ussuri": mock_gnocchi_ussuri, - "gnocchi_xena": mock_gnocchi_xena, - "designate_bind_ussuri": mock_designate_bind_ussuri, - "rabbitmq_server": mock_rmq, - "unknown_rabbitmq_server": mock_rmq_unknown, - "keystone_ussuri_cs": mock_keystone_ussuri_cs, - "keystone_wallaby": mock_keystone_wallaby, - "unknown_app": mock_unknown_app, - "mysql_router": mock_mysql_router, - "vault": mock_vault, - "keystone-ldap": mock_keystone_ldap, - "keystone-ldap-cs": mock_keystone_ldap_cs, - "nova_ussuri": mock_nova_ussuri, - "nova_wallaby": mock_nova_wallaby, - "ceph-mon_ussuri": mock_ceph_mon_ussuri, - "ceph-mon_xena": mock_ceph_mon_xena, - "ceph_osd_ussuri": mock_ceph_osd_ussuri, - "ceph_dashboard_ussuri": mock_ceph_dashboard_ussuri, - "ceph_dashboard_xena": mock_ceph_dashboard_xena, - "cinder_ussuri_on_nova": mock_cinder_on_nova, - "mysql-innodb-cluster": mock_mysql_innodb_cluster_ussuri, - "ovn_central_ussuri_22": mock_ovn_central_ussuri_22, - "ovn_central_ussuri_20": mock_ovn_central_ussuri_20, - "ovn_chassis_ussuri_22": mock_ovn_chassis_ussuri_22, - "ovn_chassis_ussuri_20": mock_ovn_chassis_ussuri_20, - } - return status + units_machines_workloads = zip(units, machines, workload_versions) + app_mock.units = _generate_units(units_machines_workloads) + return app_mock @pytest.fixture @@ -418,7 +475,7 @@ def full_status(status, model): @pytest.fixture -def units(): +def units(apps_machines): units_ussuri = [] units_wallaby = [] units_ussuri.append( @@ -426,7 +483,7 @@ def units(): name="keystone/0", os_version=OpenStackRelease("ussuri"), workload_version="17.0.1", - machine="0/lxd/12", + machine=apps_machines["keystone"]["0/lxd/12"], ) ) units_ussuri.append( @@ -434,7 +491,7 @@ def units(): name="keystone/1", os_version=OpenStackRelease("ussuri"), workload_version="17.0.1", - machine="1/lxd/12", + machine=apps_machines["keystone"]["1/lxd/12"], ) ) units_ussuri.append( @@ -442,7 +499,7 @@ def units(): name="keystone/2", os_version=OpenStackRelease("ussuri"), workload_version="17.0.1", - machine="2/lxd/13", + machine=apps_machines["keystone"]["2/lxd/13"], ) ) units_wallaby.append( @@ -450,7 +507,7 @@ def units(): name="keystone/0", os_version=OpenStackRelease("wallaby"), workload_version="19.1.0", - machine="0/lxd/12", + machine=apps_machines["keystone"]["0/lxd/12"], ) ) units_wallaby.append( @@ -458,7 +515,7 @@ def units(): name="keystone/1", os_version=OpenStackRelease("wallaby"), workload_version="19.1.0", - machine="1/lxd/12", + machine=apps_machines["keystone"]["1/lxd/12"], ) ) units_wallaby.append( @@ -466,7 +523,7 @@ def units(): name="keystone/2", os_version=OpenStackRelease("wallaby"), workload_version="19.1.0", - machine="2/lxd/13", + machine=apps_machines["keystone"]["2/lxd/13"], ) ) return {"units_ussuri": units_ussuri, "units_wallaby": units_wallaby} @@ -502,8 +559,11 @@ async def get_charm_name(value: str): @pytest.fixture -def model(config): +def model(config, apps_machines): """Define test COUModel object.""" + machines = {} + for sub_machines in apps_machines.values(): + machines = {**machines, **sub_machines} model_name = "test_model" from cou.utils import juju_utils @@ -517,6 +577,7 @@ def model(config): model.scp_from_unit = AsyncMock() model.get_application_config = mock_get_app_config = AsyncMock() mock_get_app_config.side_effect = config.get + model.get_model_machines = machines return model From f3e07f950e05f37d1b87b77a6a440e03c7b07061 Mon Sep 17 00:00:00 2001 From: Gabriel Cocenza Date: Tue, 23 Jan 2024 21:43:36 -0300 Subject: [PATCH 2/6] - fixing unit tests --- cou/apps/auxiliary.py | 2 +- tests/unit/apps/test_auxiliary.py | 88 +- tests/unit/apps/test_auxiliary_subordinate.py | 21 +- tests/unit/apps/test_channel_based.py | 39 +- tests/unit/apps/test_core.py | 13 +- tests/unit/apps/test_factory.py | 3 +- tests/unit/apps/test_subordinate.py | 35 +- tests/unit/conftest.py | 765 +++++++++--------- tests/unit/steps/test_steps_analyze.py | 60 +- tests/unit/steps/test_steps_plan.py | 14 +- 10 files changed, 573 insertions(+), 467 deletions(-) diff --git a/cou/apps/auxiliary.py b/cou/apps/auxiliary.py index 8c0f9290..423d07cd 100644 --- a/cou/apps/auxiliary.py +++ b/cou/apps/auxiliary.py @@ -182,7 +182,7 @@ def pre_upgrade_steps(self, target: OpenStackRelease) -> list[PreUpgradeStep]: return super().pre_upgrade_steps(target) -@AppFactory.register_application(["mysql-innodb-cluster"]) +@AppFactory.register_application(["mysql_innodb_cluster"]) class MysqlInnodbClusterApplication(OpenStackAuxiliaryApplication): """Application for mysql-innodb-cluster charm.""" diff --git a/tests/unit/apps/test_auxiliary.py b/tests/unit/apps/test_auxiliary.py index 98674cfb..f3c54f73 100644 --- a/tests/unit/apps/test_auxiliary.py +++ b/tests/unit/apps/test_auxiliary.py @@ -34,14 +34,14 @@ from tests.unit.apps.utils import add_steps -def test_auxiliary_app(status, config, model): +def test_auxiliary_app(status, config, model, apps_machines): # version 3.8 on rabbitmq can be from ussuri to yoga. In that case it will be set as yoga. expected_units = [ ApplicationUnit( name="rabbitmq-server/0", os_version=OpenStackRelease("yoga"), workload_version="3.8", - machine="0/lxd/19", + machine=apps_machines["rmq"]["0/lxd/19"], ) ] @@ -51,6 +51,7 @@ def test_auxiliary_app(status, config, model): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) assert app.channel == "3.8/stable" assert app.is_valid_track(app.channel) is True @@ -62,13 +63,13 @@ def test_auxiliary_app(status, config, model): assert app.current_os_release == "yoga" -def test_auxiliary_app_cs(status, config, model): +def test_auxiliary_app_cs(status, config, model, apps_machines): expected_units = [ ApplicationUnit( name="rabbitmq-server/0", os_version=OpenStackRelease("yoga"), workload_version="3.8", - machine="0/lxd/19", + machine=apps_machines["rmq"]["0/lxd/19"], ) ] rmq_status = status["rabbitmq_server"] @@ -80,6 +81,7 @@ def test_auxiliary_app_cs(status, config, model): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) assert app.channel == "stable" assert app.is_valid_track(app.channel) is True @@ -90,7 +92,9 @@ def test_auxiliary_app_cs(status, config, model): assert app.current_os_release == "yoga" -def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel(status, config, model): +def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") app = RabbitMQServer( "rabbitmq-server", @@ -98,6 +102,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel(status, config config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -157,7 +162,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel(status, config assert upgrade_plan == expected_plan -def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model): +def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): target = OpenStackRelease("victoria") rmq_status = status["rabbitmq_server"] # rabbitmq already on channel 3.9 on ussuri @@ -168,6 +173,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -221,7 +227,9 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model): assert upgrade_plan == expected_plan -def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model): +def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") rmq_status = status["rabbitmq_server"] rmq_status.charm = "cs:amd64/focal/rabbitmq-server-638" @@ -232,6 +240,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( @@ -288,7 +297,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, assert upgrade_plan == expected_plan -def test_auxiliary_upgrade_plan_unknown_track(status, config, model): +def test_auxiliary_upgrade_plan_unknown_track(status, config, model, apps_machines): rmq_status = status["rabbitmq_server"] # 2.0 is an unknown track rmq_status.charm_channel = "2.0/stable" @@ -299,10 +308,13 @@ def test_auxiliary_upgrade_plan_unknown_track(status, config, model): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) -def test_auxiliary_app_unknown_version_raise_ApplicationError(status, config, model): +def test_auxiliary_app_unknown_version_raise_ApplicationError( + status, config, model, apps_machines +): with pytest.raises(ApplicationError): RabbitMQServer( "rabbitmq-server", @@ -310,10 +322,11 @@ def test_auxiliary_app_unknown_version_raise_ApplicationError(status, config, mo config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) -def test_auxiliary_raise_error_unknown_series(status, config, model): +def test_auxiliary_raise_error_unknown_series(status, config, model, apps_machines): app_status = status["rabbitmq_server"] app_status.series = "foo" with pytest.raises(ApplicationError): @@ -323,10 +336,11 @@ def test_auxiliary_raise_error_unknown_series(status, config, model): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) -def test_auxiliary_raise_error_os_not_on_lookup(status, config, model, mocker): +def test_auxiliary_raise_error_os_not_on_lookup(status, config, model, mocker, apps_machines): # change OpenStack release to a version that is not on openstack_to_track_mapping.csv mocker.patch( "cou.apps.core.OpenStackApplication.current_os_release", @@ -340,12 +354,13 @@ def test_auxiliary_raise_error_os_not_on_lookup(status, config, model, mocker): config["auxiliary_ussuri"], model, "rabbitmq-server", + apps_machines["rmq"], ) with pytest.raises(ApplicationError): app.possible_current_channels -def test_auxiliary_raise_halt_upgrade(status, config, model): +def test_auxiliary_raise_halt_upgrade(status, config, model, apps_machines): target = OpenStackRelease("victoria") # source is already configured to wallaby, so the plan halt with target victoria app = RabbitMQServer( @@ -354,12 +369,13 @@ def test_auxiliary_raise_halt_upgrade(status, config, model): config["auxiliary_wallaby"], model, "rabbitmq-server", + apps_machines["rmq"], ) with pytest.raises(HaltUpgradePlanGeneration): app.generate_upgrade_plan(target) -def test_auxiliary_no_suitable_channel(status, config, model): +def test_auxiliary_no_suitable_channel(status, config, model, apps_machines): # OPENSTACK_TO_TRACK_MAPPING can't find a track for rabbitmq, focal, zed. target = OpenStackRelease("zed") app_status = status["rabbitmq_server"] @@ -370,19 +386,21 @@ def test_auxiliary_no_suitable_channel(status, config, model): config["auxiliary_wallaby"], model, "rabbitmq-server", + apps_machines["rmq"], ) with pytest.raises(ApplicationError): app.target_channel(target) -def test_ceph_mon_app(status, config, model): +def test_ceph_mon_app(status, config, model, apps_machines): """Test the correctness of instantiating CephMonApplication.""" app = CephMonApplication( "ceph-mon", - status["ceph-mon_xena"], + status["ceph_mon_pacific"], config["auxiliary_xena"], model, "ceph-mon", + apps_machines["ceph-mon"], ) assert app.channel == "pacific/stable" assert app.os_origin == "cloud:focal-xena" @@ -391,7 +409,7 @@ def test_ceph_mon_app(status, config, model): name="ceph-mon/0", os_version=OpenStackRelease("xena"), workload_version="16.2.0", - machine="7", + machine=apps_machines["ceph-mon"]["6"], ) ] assert app.apt_source_codename == "xena" @@ -399,19 +417,16 @@ def test_ceph_mon_app(status, config, model): assert app.is_subordinate is False -def test_ceph_mon_upgrade_plan_xena_to_yoga( - status, - config, - model, -): +def test_ceph_mon_upgrade_plan_xena_to_yoga(status, config, model, apps_machines): """Test when ceph version changes between os releases.""" target = OpenStackRelease("yoga") app = CephMonApplication( "ceph-mon", - status["ceph-mon_xena"], + status["ceph_mon_pacific"], config["auxiliary_xena"], model, "ceph-mon", + apps_machines["ceph-mon"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -478,15 +493,17 @@ def test_ceph_mon_upgrade_plan_ussuri_to_victoria( status, config, model, + apps_machines, ): """Test when ceph version remains the same between os releases.""" target = OpenStackRelease("victoria") app = CephMonApplication( "ceph-mon", - status["ceph-mon_ussuri"], + status["ceph_mon_octopus"], config["auxiliary_ussuri"], model, "ceph-mon", + apps_machines["ceph-mon"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -543,13 +560,14 @@ def test_ceph_mon_upgrade_plan_ussuri_to_victoria( assert upgrade_plan == expected_plan -def test_ovn_principal(status, config, model): +def test_ovn_principal(status, config, model, apps_machines): app = OvnPrincipalApplication( "ovn-central", - status["ovn_central_ussuri_22"], + status["ovn_central_22"], config["auxiliary_ussuri"], model, "ovn-central", + apps_machines["ovn-central"], ) assert app.channel == "22.03/stable" assert app.os_origin == "distro" @@ -559,7 +577,7 @@ def test_ovn_principal(status, config, model): assert app.is_subordinate is False -def test_ovn_workload_ver_lower_than_22_principal(status, config, model): +def test_ovn_workload_ver_lower_than_22_principal(status, config, model, apps_machines): target = OpenStackRelease("victoria") exp_error_msg_ovn_upgrade = ( @@ -571,10 +589,11 @@ def test_ovn_workload_ver_lower_than_22_principal(status, config, model): app_ovn_central = OvnPrincipalApplication( "ovn-central", - status["ovn_central_ussuri_20"], + status["ovn_central_20"], config["auxiliary_ussuri"], model, "ovn-central", + apps_machines["ovn-central"], ) with pytest.raises(ApplicationError, match=exp_error_msg_ovn_upgrade): @@ -582,8 +601,8 @@ def test_ovn_workload_ver_lower_than_22_principal(status, config, model): @pytest.mark.parametrize("channel", ["55.7", "19.03"]) -def test_ovn_no_compatible_os_release(status, config, model, channel): - ovn_central_status = status["ovn_central_ussuri_22"] +def test_ovn_no_compatible_os_release(status, config, model, channel, apps_machines): + ovn_central_status = status["ovn_central_22"] ovn_central_status.charm_channel = channel with pytest.raises(ApplicationError): OvnPrincipalApplication( @@ -592,17 +611,19 @@ def test_ovn_no_compatible_os_release(status, config, model, channel): config["auxiliary_ussuri"], model, "ovn-central", + apps_machines["ovn-central"], ) -def test_ovn_principal_upgrade_plan(status, config, model): +def test_ovn_principal_upgrade_plan(status, config, model, apps_machines): target = OpenStackRelease("victoria") app = OvnPrincipalApplication( "ovn-central", - status["ovn_central_ussuri_22"], + status["ovn_central_22"], config["auxiliary_ussuri"], model, "ovn-central", + apps_machines["ovn-central"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -656,15 +677,16 @@ def test_ovn_principal_upgrade_plan(status, config, model): assert upgrade_plan == expected_plan -def test_mysql_innodb_cluster_upgrade(status, config, model): +def test_mysql_innodb_cluster_upgrade(status, config, model, apps_machines): target = OpenStackRelease("victoria") # source is already configured to wallaby, so the plan halt with target victoria app = MysqlInnodbClusterApplication( "mysql-innodb-cluster", - status["mysql-innodb-cluster"], + status["mysql_innodb_cluster"], config["auxiliary_ussuri"], model, "mysql-innodb-cluster", + apps_machines["mysql-innodb-cluster"], ) upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( diff --git a/tests/unit/apps/test_auxiliary_subordinate.py b/tests/unit/apps/test_auxiliary_subordinate.py index e728e899..19dad9d1 100644 --- a/tests/unit/apps/test_auxiliary_subordinate.py +++ b/tests/unit/apps/test_auxiliary_subordinate.py @@ -57,11 +57,7 @@ def test_auxiliary_subordinate_upgrade_plan_to_victoria(apps, model): def test_ovn_subordinate(status, model): app = OvnSubordinateApplication( - "ovn-chassis", - status["ovn_chassis_ussuri_22"], - {}, - model, - "ovn-chassis", + "ovn-chassis", status["ovn_chassis_focal_22"], {}, model, "ovn-chassis", {} ) assert app.channel == "22.03/stable" assert app.os_origin == "" @@ -83,10 +79,11 @@ def test_ovn_workload_ver_lower_than_22_subordinate(status, model): app_ovn_chassis = OvnSubordinateApplication( "ovn-chassis", - status["ovn_chassis_ussuri_20"], + status["ovn_chassis_focal_20"], {}, model, "ovn-chassis", + {}, ) with pytest.raises(ApplicationError, match=exp_error_msg_ovn_upgrade): @@ -97,10 +94,11 @@ def test_ovn_subordinate_upgrade_plan(status, model): target = OpenStackRelease("victoria") app = OvnSubordinateApplication( "ovn-chassis", - status["ovn_chassis_ussuri_22"], + status["ovn_chassis_focal_22"], {}, model, "ovn-chassis", + {}, ) upgrade_plan = app.generate_upgrade_plan(target) @@ -125,7 +123,7 @@ def test_ovn_subordinate_upgrade_plan_cant_upgrade_charm(status, model): # ovn chassis 22.03 is considered yoga. If it's not necessary to upgrade # the charm code, there is no steps to upgrade. target = OpenStackRelease("victoria") - app_status = status["ovn_chassis_ussuri_22"] + app_status = status["ovn_chassis_focal_22"] app_status.can_upgrade_to = "" app = OvnSubordinateApplication( "ovn-chassis", @@ -133,6 +131,7 @@ def test_ovn_subordinate_upgrade_plan_cant_upgrade_charm(status, model): {}, model, "ovn-chassis", + {}, ) expected_plan = ApplicationUpgradePlan( @@ -149,10 +148,11 @@ def test_ceph_dashboard_upgrade_plan_ussuri_to_victoria(status, config, model): target = OpenStackRelease("victoria") app = OpenStackAuxiliarySubordinateApplication( "ceph-dashboard", - status["ceph_dashboard_ussuri"], + status["ceph_dashboard_octopus"], config["auxiliary_ussuri"], model, "ceph-dashboard", + {}, ) upgrade_plan = app.generate_upgrade_plan(target) @@ -178,10 +178,11 @@ def test_ceph_dashboard_upgrade_plan_xena_to_yoga(status, config, model): target = OpenStackRelease("yoga") app = OpenStackAuxiliarySubordinateApplication( "ceph-dashboard", - status["ceph_dashboard_xena"], + status["ceph_dashboard_pacific"], config["auxiliary_xena"], model, "ceph-dashboard", + {}, ) upgrade_plan = app.generate_upgrade_plan(target) diff --git a/tests/unit/apps/test_channel_based.py b/tests/unit/apps/test_channel_based.py index b4aca622..832a60eb 100644 --- a/tests/unit/apps/test_channel_based.py +++ b/tests/unit/apps/test_channel_based.py @@ -23,71 +23,78 @@ from tests.unit.apps.utils import add_steps -def test_application_versionless(status, config, model): +def test_application_versionless(status, config, model, apps_machines): app = OpenStackChannelBasedApplication( "glance-simplestreams-sync", - status["glance_simplestreams_sync_ussuri"], + status["glance_simplestreams_sync_focal_ussuri"], config["openstack_ussuri"], model, "glance-simplestreams-sync", + apps_machines["glance-simplestreams-sync"], ) assert app.current_os_release == "ussuri" assert app.is_versionless is True -def test_application_gnocchi_ussuri(status, config, model): +def test_application_gnocchi_ussuri(status, config, model, apps_machines): app = OpenStackChannelBasedApplication( "gnocchi", - status["gnocchi_ussuri"], + status["gnocchi_focal_ussuri"], config["openstack_ussuri"], model, "gnocchi", + apps_machines["gnocchi"], ) assert app.current_os_release == "ussuri" assert app.is_versionless is False -def test_application_gnocchi_xena(status, config, model): +def test_application_gnocchi_xena(status, config, model, apps_machines): # workload version is the same for xena and yoga, but current_os_release # is based on the channel. app = OpenStackChannelBasedApplication( "gnocchi", - status["gnocchi_xena"], + status["gnocchi_focal_xena"], config["openstack_xena"], model, "gnocchi", + apps_machines["gnocchi"], ) assert app.current_os_release == "xena" assert app.is_versionless is False -def test_application_designate_bind_ussuri(status, config, model): +def test_application_designate_bind_ussuri(status, config, model, apps_machines): # workload version is the same from ussuri to yoga, but current_os_release # is based on the channel. app_config = config["openstack_ussuri"] app_config["action-managed-upgrade"] = {"value": False} app = OpenStackChannelBasedApplication( "designate-bind", - status["designate_bind_ussuri"], + status["designate_bind_focal_ussuri"], app_config, model, "designate-bind", + apps_machines["designate-bind"], ) assert app.current_os_release == "ussuri" assert app.is_versionless is False -def test_application_versionless_upgrade_plan_ussuri_to_victoria(status, config, model): +def test_application_versionless_upgrade_plan_ussuri_to_victoria( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") app_config = config["openstack_ussuri"] # Does not have action-managed-upgrade app_config.pop("action-managed-upgrade") app = OpenStackChannelBasedApplication( "glance-simplestreams-sync", - status["glance_simplestreams_sync_ussuri"], + status["glance_simplestreams_sync_focal_ussuri"], app_config, model, "glance-simplestreams-sync", + apps_machines["glance-simplestreams-sync"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -138,17 +145,18 @@ def test_application_versionless_upgrade_plan_ussuri_to_victoria(status, config, assert upgrade_plan == expected_plan -def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(status, config, model): +def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): # Gnocchi from ussuri to victoria upgrade the workload version from 4.3.4 to 4.4.0. target = OpenStackRelease("victoria") app_config = config["openstack_ussuri"] app_config["action-managed-upgrade"] = {"value": False} app = OpenStackChannelBasedApplication( "gnocchi", - status["gnocchi_ussuri"], + status["gnocchi_focal_ussuri"], app_config, model, "gnocchi", + apps_machines["gnocchi"], ) upgrade_plan = app.generate_upgrade_plan(target) @@ -209,16 +217,19 @@ def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(status, config, mod assert upgrade_plan == expected_plan -def test_application_designate_bind_upgrade_plan_ussuri_to_victoria(status, config, model): +def test_application_designate_bind_upgrade_plan_ussuri_to_victoria( + status, config, model, apps_machines +): target = OpenStackRelease("victoria") app_config = config["openstack_ussuri"] app_config["action-managed-upgrade"] = {"value": False} app = OpenStackChannelBasedApplication( "designate-bind", - status["designate_bind_ussuri"], + status["designate_bind_focal_ussuri"], app_config, model, "designate-bind", + apps_machines["designate-bind"], ) upgrade_plan = app.generate_upgrade_plan(target) diff --git a/tests/unit/apps/test_core.py b/tests/unit/apps/test_core.py index 71e48c88..39648be8 100644 --- a/tests/unit/apps/test_core.py +++ b/tests/unit/apps/test_core.py @@ -166,7 +166,8 @@ def test_application_different_wl(status, config, model, apps_machines): r"'ussuri': \['keystone\/0', 'keystone\/1'\], 'victoria': \['keystone\/2'\]. " "This is not currently handled." ) - app_status = status["keystone_focal_ussuri_victoria"] + app_status = status["keystone_focal_ussuri"] + app_status.units["keystone/2"].workload_version = "18.1.0" app_config = config["openstack_ussuri"] app = Keystone( @@ -179,7 +180,10 @@ def test_application_different_wl(status, config, model, apps_machines): def test_application_cs(status, config, units, model, apps_machines): """Test when application is from charm store.""" target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri_cs"] + + app_status = status["keystone_focal_ussuri"] + app_status.charm = "cs:amd64/focal/keystone-638" + app_config = config["openstack_ussuri"] exp_os_origin = "distro" exp_units = units["units_ussuri"] @@ -443,7 +447,10 @@ def test_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model, apps_machines): target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri_cs"] + + app_status = status["keystone_focal_ussuri"] + app_status.charm = "cs:amd64/focal/keystone-638" + app_config = config["openstack_ussuri"] app = Keystone( "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] diff --git a/tests/unit/apps/test_factory.py b/tests/unit/apps/test_factory.py index 194ceb1c..1f97f614 100644 --- a/tests/unit/apps/test_factory.py +++ b/tests/unit/apps/test_factory.py @@ -25,6 +25,7 @@ def test_app_factory_not_supported_openstack_charm(mock_is_charm_supported): config=MagicMock(), model=MagicMock(), charm=charm, + machines=MagicMock(), ) assert my_app is None mock_is_charm_supported.assert_called_once_with(charm) @@ -40,7 +41,7 @@ def __init__(self, *_, **__): pass assert charm in factory.AppFactory.charms - foo = factory.AppFactory.create("my-foo", MagicMock(), {}, MagicMock(), charm) + foo = factory.AppFactory.create("my-foo", MagicMock(), {}, MagicMock(), charm, MagicMock()) mock_is_charm_supported.assert_called_once_with(charm) assert foo is not None assert isinstance(foo, Foo) diff --git a/tests/unit/apps/test_subordinate.py b/tests/unit/apps/test_subordinate.py index 4afda24c..3562cf50 100644 --- a/tests/unit/apps/test_subordinate.py +++ b/tests/unit/apps/test_subordinate.py @@ -26,9 +26,9 @@ def test_post_init(status, model): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) assert app.channel == "ussuri/stable" assert app.charm_origin == "ch" @@ -37,18 +37,18 @@ def test_post_init(status, model): def test_current_os_release(status, model): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) assert app.current_os_release == OpenStackRelease("ussuri") def test_generate_upgrade_plan(status, model): target = OpenStackRelease("victoria") - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) upgrade_plan = app.generate_upgrade_plan(target) @@ -84,9 +84,9 @@ def test_generate_upgrade_plan(status, model): ], ) def test_channel_setter_valid(status, model, channel): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) app.channel = channel @@ -103,10 +103,12 @@ def test_channel_setter_valid(status, model, channel): ], ) def test_channel_setter_invalid(status, model, channel): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app_status.charm_channel = channel with pytest.raises(ApplicationError): - OpenStackSubordinateApplication("my_keystone_ldap", app_status, {}, model, "keystone-ldap") + OpenStackSubordinateApplication( + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + ) @pytest.mark.parametrize( @@ -119,9 +121,10 @@ def test_channel_setter_invalid(status, model, channel): ) def test_generate_plan_ch_migration(status, model, channel): target = OpenStackRelease("wallaby") - app_status = status["keystone-ldap-cs"] + app_status = status["keystone_ldap_focal_ussuri"] + app_status.charm = "cs:amd64/focal/keystone-ldap-437" app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) app.channel = channel @@ -157,9 +160,9 @@ def test_generate_plan_ch_migration(status, model, channel): ], ) def test_generate_plan_from_to(status, model, from_os, to_os): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) app.channel = f"{from_os}/stable" @@ -194,9 +197,9 @@ def test_generate_plan_from_to(status, model, from_os, to_os): ], ) def test_generate_plan_in_same_version(status, model, from_to): - app_status = status["keystone-ldap"] + app_status = status["keystone_ldap_focal_ussuri"] app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap" + "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} ) app.channel = f"{from_to}/stable" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 158d0465..b9d21a2f 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -13,6 +13,7 @@ # limitations under the License. from collections import OrderedDict +from itertools import zip_longest from pathlib import Path from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch @@ -34,12 +35,53 @@ KEYSTONE_UNITS = ["keystone/0", "keystone/1", "keystone/2"] KEYSTONE_MACHINES = ["0/lxd/12", "1/lxd/12", "2/lxd/13"] KEYSTONE_WORKLOADS = { - "ussuri": ["17.0.1", "17.0.1", "17.0.1"], - "victoria": ["18.1.0", "18.1.0", "18.1.0"], - "ussuri-victoria": ["17.0.1", "17.0.1", "18.1.0"], - "wallaby": ["19.1.0", "19.1.0", "19.1.0"], + "ussuri": "17.0.1", + "victoria": "18.1.0", + "wallaby": "19.1.0", } +CINDER_UNITS = ["cinder/0", "cinder/1", "cinder/2"] +CINDER_MACHINES = ["0/lxd/5", "1/lxd/5", "2/lxd/5"] +CINDER_WORKLOADS = {"ussuri": "16.4.2"} + +NOVA_UNITS = ["nova-compute/0", "nova-compute/1", "nova-compute/2"] +NOVA_MACHINES = ["0", "1", "2"] +NOVA_WORKLOADS = {"ussuri": "21.0.0"} + +RMQ_UNITS = ["rabbitmq-server/0"] +RMQ_MACHINES = ["0/lxd/19"] +RMQ_WORKLOADS = {"3.8": "3.8"} + +CEPH_MON_UNITS = ["ceph-mon/0"] +CEPH_MON_MACHINES = ["6"] + +CEPH_OSD_UNITS = ["ceph-osd/0"] +CEPH_OSD_MACHINES = ["7"] + +CEPH_WORKLOADS = {"octopus": "15.2.0", "pacific": "16.2.0"} + +OVN_UNITS = ["ovn-central/0"] +OVN_MACHINES = ["0/lxd/7"] +OVN_WORKLOADS = {"22.03": "22.03.2", "20.03": "20.03.2"} + +MYSQL_UNITS = ["mysql/0"] +MYSQL_MACHINES = ["0/lxd/7"] +MYSQL_WORKLOADS = {"8.0": "8.0"} + +GLANCE_SIMPLE_UNITS = ["glance-simplestreams-sync/0"] +GLANCE_SIMPLE_MACHINES = ["4/lxd/5"] + +DESIGNATE_UNITS = ["designate-bind/0", "designate-bind/1"] +DESIGNATE_MACHINES = ["1/lxd/6", "2/lxd/6"] +DESIGNATE_WORKLOADS = {"ussuri": "9.16.1"} + +GNOCCHI_UNITS = ["gnocchi/0", "gnocchi/1", "gnocchi/2"] +GNOCCHI_MACHINES = ["3/lxd/6", "4/lxd/6", "5/lxd/5"] +GNOCCHI_WORKLOADS = {"ussuri": "4.3.4", "xena": "4.4.1"} + +MY_APP_UNITS = ["my-app/0"] +MY_APP_MACHINES = ["0/lxd/11"] + def _generate_unit(workload_version, machine): unit = MagicMock(spec_set=UnitStatus()) @@ -62,15 +104,28 @@ def _generate_units(units_machines_workloads): @pytest.fixture def apps_machines(): return { + **_generate_apps_machines("keystone", KEYSTONE_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("cinder", CINDER_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("nova-compute", NOVA_MACHINES, STANDARD_AZS, True), + **_generate_apps_machines("rmq", RMQ_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("ceph-mon", CEPH_MON_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("ovn-central", OVN_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("mysql-innodb-cluster", MYSQL_MACHINES, STANDARD_AZS, False), **_generate_apps_machines( - "keystone", KEYSTONE_MACHINES, STANDARD_AZS, [False, False, False] + "glance-simplestreams-sync", GLANCE_SIMPLE_MACHINES, STANDARD_AZS, False ), + **_generate_apps_machines("gnocchi", GNOCCHI_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("designate-bind", DESIGNATE_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("ceph-osd", CEPH_OSD_MACHINES, STANDARD_AZS, True), + **_generate_apps_machines("my-app", MY_APP_MACHINES, STANDARD_AZS, False), } def _generate_apps_machines(charm, machines, azs, is_data_plane): hostnames = [f"{HOSTNAME_PREFIX}-{machine}" for machine in machines] - machines_hostnames_azs_is_data_plane = zip(machines, hostnames, azs, is_data_plane) + machines_hostnames_azs_is_data_plane = zip_longest( + machines, hostnames, azs, [is_data_plane], fillvalue=is_data_plane + ) return { charm: { machine_id: Machine( @@ -83,376 +138,334 @@ def _generate_apps_machines(charm, machines, azs, is_data_plane): @pytest.fixture def status(): - # mock_cinder_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_cinder_ussuri.series = "focal" - # mock_cinder_ussuri.charm_channel = "ussuri/stable" - # mock_cinder_ussuri.charm = "ch:amd64/focal/cinder-633" - # mock_cinder_ussuri.subordinate_to = [] - # mock_cinder_ussuri.units = OrderedDict( - # [ - # ("cinder/0", generate_unit("cinder", "16.4.2", "0/lxd/5")), - # ("cinder/1", generate_unit("cinder", "16.4.2", "1/lxd/5")), - # ("cinder/2", generate_unit("cinder", "16.4.2", "2/lxd/5")), - # ] - # ) - - # mock_cinder_on_nova = MagicMock(spec_set=ApplicationStatus()) - # mock_cinder_on_nova.series = "focal" - # mock_cinder_on_nova.charm_channel = "ussuri/stable" - # mock_cinder_on_nova.charm = "ch:amd64/focal/cinder-633" - # mock_cinder_on_nova.subordinate_to = [] - # mock_cinder_on_nova.units = OrderedDict( - # [ - # ("cinder/0", generate_unit("cinder-nova", "16.4.2", "0")), - # ("cinder/1", generate_unit("cinder-nova", "16.4.2", "1")), - # ("cinder/2", generate_unit("cinder-nova", "16.4.2", "2")), - # ] - # ) - - # # gnocchi on ussuri - # mock_gnocchi_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_gnocchi_ussuri.series = "focal" - # mock_gnocchi_ussuri.charm_channel = "ussuri/stable" - # mock_gnocchi_ussuri.charm = "ch:amd64/focal/gnocchi-638" - # mock_gnocchi_ussuri.subordinate_to = [] - # mock_gnocchi_ussuri.units = OrderedDict( - # [ - # ("gnocchi/0", generate_unit("gnocchi", "4.3.4", "3/lxd/6")), - # ("gnocchi/1", generate_unit("gnocchi", "4.3.4", "4/lxd/6")), - # ("gnocchi/2", generate_unit("gnocchi", "4.3.4", "5/lxd/5")), - # ] - # ) - - # # gnocchi on xena - # mock_gnocchi_xena = MagicMock(spec_set=ApplicationStatus()) - # mock_gnocchi_xena.series = "focal" - # mock_gnocchi_xena.charm_channel = "xena/stable" - # mock_gnocchi_xena.charm = "ch:amd64/focal/gnocchi-638" - # mock_gnocchi_xena.subordinate_to = [] - # mock_gnocchi_xena.units = OrderedDict( - # [ - # ("gnocchi/0", generate_unit("gnocchi", "4.4.1", "3/lxd/6")), - # ("gnocchi/1", generate_unit("gnocchi", "4.4.1", "4/lxd/6")), - # ("gnocchi/2", generate_unit("gnocchi", "4.4.1", "5/lxd/5")), - # ] - # ) - - # # designate-bind on ussuri - # mock_designate_bind_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_designate_bind_ussuri.series = "focal" - # mock_designate_bind_ussuri.charm_channel = "ussuri/stable" - # mock_designate_bind_ussuri.charm = "ch:amd64/focal/designate-bind-737" - # mock_designate_bind_ussuri.subordinate_to = [] - # mock_designate_bind_ussuri.units = OrderedDict( - # [ - # ("designate-bind/0", generate_unit("designate-bind", "9.16.1", "1/lxd/6")), - # ("designate-bind/1", generate_unit("designate-bind","9.16.1", "2/lxd/6")), - # ] - # ) - - # mock_nova_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_nova_ussuri.series = "focal" - # mock_nova_ussuri.charm_channel = "ussuri/stable" - # mock_nova_ussuri.charm = "ch:amd64/focal/nova-compute-638" - # mock_nova_ussuri.subordinate_to = [] - # mock_nova_ussuri.units = OrderedDict( - # [ - # ("nova-compute/0", generate_unit("nova-compute", "21.0.0", "0")), - # ("nova-compute/1", generate_unit("nova-compute", "21.0.0", "1")), - # ("nova-compute/2", generate_unit("nova-compute", "21.0.0", "2")), - # ] - # ) - - # mock_nova_wallaby = MagicMock(spec_set=ApplicationStatus()) - # mock_nova_wallaby.series = "focal" - # mock_nova_wallaby.charm_channel = "wallaby/stable" - # mock_nova_wallaby.charm = "ch:amd64/focal/nova-compute-638" - # mock_nova_wallaby.subordinate_to = [] - # mock_nova_wallaby.units = OrderedDict( - # [ - # ("nova-compute/0", generate_unit("nova-compute", "24.1.0", "0")), - # ("nova-compute/1", generate_unit("nova-compute", "24.1.0", "1")), - # ("nova-compute/2", generate_unit("nova-compute", "24.1.0", "2")), - # ] - # ) - - # # glance-simplestreams-sync does not have workload_version - # mock_glance_simplestreams_sync_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_glance_simplestreams_sync_ussuri.series = "focal" - # mock_glance_simplestreams_sync_ussuri.charm_channel = "ussuri/stable" - # mock_glance_simplestreams_sync_ussuri.charm = "ch:amd64/focal/glance-simplestreams-sync-78" - # mock_glance_simplestreams_sync_ussuri.subordinate_to = [] - # mock_glance_simplestreams_sync_ussuri.units = OrderedDict( - # [ - # ("glance-simplestreams-sync/0", generate_unit("glance-simplestreams-sync", "", "4/lxd/5")), - # ] - # ) - - # mock_rmq = MagicMock(spec_set=ApplicationStatus()) - # mock_rmq.series = "focal" - # mock_rmq.charm_channel = "3.8/stable" - # mock_rmq.charm = "ch:amd64/focal/rabbitmq-server-638" - # mock_rmq.subordinate_to = [] - # mock_rmq.units = OrderedDict([("rabbitmq-server/0", generate_unit("rabbitmq-server", "3.8", "0/lxd/19"))]) - - # mock_rmq_unknown = MagicMock(spec_set=ApplicationStatus()) - # mock_rmq_unknown.series = "focal" - # mock_rmq_unknown.charm_channel = "80.5/stable" - # mock_rmq_unknown.charm = "ch:amd64/focal/rabbitmq-server-638" - # mock_rmq_unknown.subordinate_to = [] - # mock_rmq_unknown.units = OrderedDict( - # [("rabbitmq-server/0", generate_unit("rabbitmq-server", "80.5", "0/lxd/19"))] - # ) - - # mock_unknown_app = MagicMock(spec_set=ApplicationStatus()) - # mock_unknown_app.series = "focal" - # mock_unknown_app.charm_channel = "12.5/stable" - # mock_unknown_app.charm = "ch:amd64/focal/my-app-638" - # mock_unknown_app.subordinate_to = [] - # mock_unknown_app.units = OrderedDict([("my-app/0", generate_unit("my-app", "12.5", "0/lxd/11"))]) - - # # openstack related principal application without openstack origin or source - # mock_vault = MagicMock(spec_set=ApplicationStatus()) - # mock_vault.series = "focal" - # mock_vault.charm_channel = "1.7/stable" - # mock_vault.charm = "ch:amd64/focal/vault-638" - # mock_vault.subordinate_to = [] - # mock_vault.units = OrderedDict([("vault/0", generate_unit("vault", "1.7", "5"))]) - - # # auxiliary subordinate application - # mock_mysql_router = MagicMock(spec_set=ApplicationStatus()) - # mock_mysql_router.series = "focal" - # mock_mysql_router.charm_channel = "8.0/stable" - # mock_mysql_router.charm = "ch:amd64/focal/mysql-router-437" - # mock_mysql_router.subordinate_to = ["keystone"] - # mock_mysql_router.units = {} - - # # OpenStack subordinate application - # mock_keystone_ldap = MagicMock(spec_set=ApplicationStatus()) - # mock_keystone_ldap.series = "focal" - # mock_keystone_ldap.charm_channel = "ussuri/stable" - # mock_keystone_ldap.charm = "ch:amd64/focal/keystone-ldap-437" - # mock_keystone_ldap.subordinate_to = ["keystone"] - # mock_keystone_ldap.units = {} - - # # OpenStack subordinate application cs - # mock_keystone_ldap_cs = MagicMock(spec_set=ApplicationStatus()) - # mock_keystone_ldap_cs.series = "focal" - # mock_keystone_ldap_cs.charm_channel = "stable" - # mock_keystone_ldap_cs.charm = "cs:amd64/focal/keystone-ldap-437" - # mock_keystone_ldap_cs.subordinate_to = ["keystone"] - # mock_keystone_ldap_cs.units = {} - - # # ceph-mon application on ussuri - # mock_ceph_mon_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_ceph_mon_ussuri.series = "focal" - # mock_ceph_mon_ussuri.charm_channel = "octopus/stable" - # mock_ceph_mon_ussuri.charm = "ch:amd64/focal/ceph-mon-177" - # mock_ceph_mon_ussuri.subordinate_to = [] - # mock_ceph_mon_ussuri.units = OrderedDict([("ceph-mon/0", generate_unit("ceph-mon", "15.2.0", "6"))]) - - # # ceph-mon application on xena - # mock_ceph_mon_xena = MagicMock(spec_set=ApplicationStatus()) - # mock_ceph_mon_xena.series = "focal" - # mock_ceph_mon_xena.charm_channel = "pacific/stable" - # mock_ceph_mon_xena.charm = "ch:amd64/focal/ceph-mon-178" - # mock_ceph_mon_xena.subordinate_to = [] - # mock_ceph_mon_xena.units = OrderedDict([("ceph-mon/0", generate_unit("ceph-mon", "16.2.0", "7"))]) - - # # ceph-osd application on ussuri - # mock_ceph_osd_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_ceph_osd_ussuri.series = "focal" - # mock_ceph_osd_ussuri.charm_channel = "octopus/stable" - # mock_ceph_osd_ussuri.charm = "ch:amd64/focal/ceph-osd-177" - # mock_ceph_osd_ussuri.subordinate_to = [] - # mock_ceph_osd_ussuri.units = OrderedDict([("ceph-osd/0", generate_unit("ceph-mon", "15.2.0", "6"))]) - - # # mysql-innodb-cluster application on ussuri using 8.0 - # mock_mysql_innodb_cluster_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_mysql_innodb_cluster_ussuri.series = "focal" - # mock_mysql_innodb_cluster_ussuri.charm_channel = "8.0/stable" - # mock_mysql_innodb_cluster_ussuri.charm = "ch:amd64/focal/mysql-innodb-cluster-106" - # mock_mysql_innodb_cluster_ussuri.subordinate_to = [] - # mock_mysql_innodb_cluster_ussuri.units = OrderedDict( - # [("ovn-central/0", generate_unit("ovn-central", "8.0", "0/lxd/7"))] - # ) - - # # ovn-central application on ussuri using 22.03 - # mock_ovn_central_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) - # mock_ovn_central_ussuri_22.series = "focal" - # mock_ovn_central_ussuri_22.charm_channel = "22.03/stable" - # mock_ovn_central_ussuri_22.charm = "ch:amd64/focal/ovn-central-178" - # mock_ovn_central_ussuri_22.subordinate_to = [] - # mock_ovn_central_ussuri_22.units = OrderedDict( - # [("ovn-central/0", generate_unit("ovn-central", "22.03.2", "0/lxd/7"))] - # ) - - # # ovn-central application on ussuri using 20.03 - # mock_ovn_central_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) - # mock_ovn_central_ussuri_20.series = "focal" - # mock_ovn_central_ussuri_20.charm_channel = "20.03/stable" - # mock_ovn_central_ussuri_20.charm = "ch:amd64/focal/ovn-central-178" - # mock_ovn_central_ussuri_20.subordinate_to = [] - # mock_ovn_central_ussuri_20.units = OrderedDict( - # [("ovn-central/0", generate_unit("ovn-central", "20.03.2", "0/lxd/7"))] - # ) - - # # ovn-chassis application on ussuri using 22.03 - # mock_ovn_chassis_ussuri_22 = MagicMock(spec_set=ApplicationStatus()) - # mock_ovn_chassis_ussuri_22.series = "focal" - # mock_ovn_chassis_ussuri_22.charm_channel = "22.03/stable" - # mock_ovn_chassis_ussuri_22.charm = "ch:amd64/focal/ovn-chassis-178" - # mock_ovn_chassis_ussuri_22.workload_version = "22.03.2" - # mock_ovn_chassis_ussuri_22.subordinate_to = ["nova-compute"] - # mock_ovn_chassis_ussuri_22.units = {} - - # # ovn-chassis application on ussuri using 20.03 - # mock_ovn_chassis_ussuri_20 = MagicMock(spec_set=ApplicationStatus()) - # mock_ovn_chassis_ussuri_20.series = "focal" - # mock_ovn_chassis_ussuri_20.charm_channel = "20.03/stable" - # mock_ovn_chassis_ussuri_20.charm = "ch:amd64/focal/ovn-chassis-178" - # mock_ovn_chassis_ussuri_20.subordinate_to = ["nova-compute"] - # mock_ovn_chassis_ussuri_20.workload_version = "20.03.2" - # mock_ovn_chassis_ussuri_20.units = {} - - # # ceph-dashboard on ussuri - # mock_ceph_dashboard_ussuri = MagicMock(spec_set=ApplicationStatus()) - # mock_ceph_dashboard_ussuri.series = "focal" - # mock_ceph_dashboard_ussuri.charm_channel = "octopus/stable" - # mock_ceph_dashboard_ussuri.charm = "ch:amd64/focal/ceph-dashboard-178" - # mock_ceph_dashboard_ussuri.subordinate_to = ["ceph-mon"] - # mock_ceph_dashboard_ussuri.units = {} - - # # ceph-dashboard on xena - # mock_ceph_dashboard_xena = MagicMock(spec_set=ApplicationStatus()) - # mock_ceph_dashboard_xena.series = "focal" - # mock_ceph_dashboard_xena.charm_channel = "pacific/stable" - # mock_ceph_dashboard_xena.charm = "ch:amd64/focal/ceph-dashboard-178" - # mock_ceph_dashboard_xena.subordinate_to = ["ceph-mon"] - # mock_ceph_dashboard_xena.units = {} - - status = { - **generate_keystone_status() - # "cinder_ussuri": mock_cinder_ussuri, - # "glance_simplestreams_sync_ussuri": mock_glance_simplestreams_sync_ussuri, - # "gnocchi_ussuri": mock_gnocchi_ussuri, - # "gnocchi_xena": mock_gnocchi_xena, - # "designate_bind_ussuri": mock_designate_bind_ussuri, - # "rabbitmq_server": mock_rmq, - # "unknown_rabbitmq_server": mock_rmq_unknown, - # "unknown_app": mock_unknown_app, - # "mysql_router": mock_mysql_router, - # "vault": mock_vault, - # "keystone-ldap": mock_keystone_ldap, - # "keystone-ldap-cs": mock_keystone_ldap_cs, - # "nova_ussuri": mock_nova_ussuri, - # "nova_wallaby": mock_nova_wallaby, - # "ceph-mon_ussuri": mock_ceph_mon_ussuri, - # "ceph-mon_xena": mock_ceph_mon_xena, - # "ceph_osd_ussuri": mock_ceph_osd_ussuri, - # "ceph_dashboard_ussuri": mock_ceph_dashboard_ussuri, - # "ceph_dashboard_xena": mock_ceph_dashboard_xena, - # "cinder_ussuri_on_nova": mock_cinder_on_nova, - # "mysql-innodb-cluster": mock_mysql_innodb_cluster_ussuri, - # "ovn_central_ussuri_22": mock_ovn_central_ussuri_22, - # "ovn_central_ussuri_20": mock_ovn_central_ussuri_20, - # "ovn_chassis_ussuri_22": mock_ovn_chassis_ussuri_22, - # "ovn_chassis_ussuri_20": mock_ovn_chassis_ussuri_20, + return { + **generate_keystone_status(), + **generate_cinder_status(), + **generate_nova_status(), + **generate_rmq_status(), + **generate_ceph_mon_status(), + **generate_ceph_osd_status(), + **generate_ovn_central_status(), + **generate_mysql_innodb_cluster_status(), + **generate_glance_simplestreams_sync_status(), + **generate_gnocchi_status(), + **generate_ovn_chassis_status(), + **generate_ceph_dashboard_status(), + **generate_keystone_ldap_status(), + **generate_designate_bind_status(), + **generate_mysql_router_status(), + **generate_my_app(), } - return status def generate_keystone_status(): - keystone_units = ["keystone/0", "keystone/1", "keystone/2"] - keystone_machines = ["0/lxd/12", "1/lxd/12", "2/lxd/13"] - - keystone_workloads = { - "ussuri": ["17.0.1", "17.0.1", "17.0.1"], - "victoria": ["18.1.0", "18.1.0", "18.1.0"], - "ussuri-victoria": ["17.0.1", "17.0.1", "18.1.0"], - "wallaby": ["19.1.0", "19.1.0", "19.1.0"], - } - mock_keystone_focal_ussuri = _generate_status( "focal", "ussuri/stable", "ch:amd64/focal/keystone-638", [], - keystone_units, - keystone_machines, - keystone_workloads["ussuri"], + KEYSTONE_UNITS, + KEYSTONE_MACHINES, + KEYSTONE_WORKLOADS["ussuri"], + ) + + mock_keystone_focal_victoria = _generate_status( + "focal", + "wallaby/stable", + "ch:amd64/focal/keystone-638", + [], + KEYSTONE_UNITS, + KEYSTONE_MACHINES, + KEYSTONE_WORKLOADS["victoria"], ) - mock_keystone_bionic_ussuri = _generate_status( - "bionic", + mock_keystone_focal_wallaby = _generate_status( + "focal", + "wallaby/stable", + "ch:amd64/focal/keystone-638", + [], + KEYSTONE_UNITS, + KEYSTONE_MACHINES, + KEYSTONE_WORKLOADS["wallaby"], + ) + + return { + "keystone_focal_ussuri": mock_keystone_focal_ussuri, + "keystone_focal_victoria": mock_keystone_focal_victoria, + "keystone_focal_wallaby": mock_keystone_focal_wallaby, + } + + +def generate_cinder_status(): + mock_cinder_focal_ussuri = _generate_status( + "focal", "ussuri/stable", - "ch:amd64/bionic/keystone-638", + "ch:amd64/focal/cinder-633", [], - keystone_units, - keystone_machines, - keystone_workloads["ussuri"], + CINDER_UNITS, + CINDER_MACHINES, + CINDER_WORKLOADS["ussuri"], ) + return {"cinder_focal_ussuri": mock_cinder_focal_ussuri} + - mock_keystone_focal_ussuri_cs = _generate_status( +def generate_nova_status(): + mock_nova_focal_ussuri = _generate_status( "focal", "ussuri/stable", - "cs:amd64/focal/keystone-638", + "ch:amd64/focal/nova-compute-638", [], - keystone_units, - keystone_machines, - keystone_workloads["ussuri"], + NOVA_UNITS, + NOVA_MACHINES, + NOVA_WORKLOADS["ussuri"], ) + return {"nova_focal_ussuri": mock_nova_focal_ussuri} - mock_keystone_focal_victoria = _generate_status( + +def generate_rmq_status(): + mock_rmq = _generate_status( "focal", - "wallaby/stable", - "ch:amd64/focal/keystone-638", + "3.8/stable", + "ch:amd64/focal/rabbitmq-server-638", [], - keystone_units, - keystone_machines, - keystone_workloads["victoria"], + RMQ_UNITS, + RMQ_MACHINES, + RMQ_WORKLOADS["3.8"], ) + mock_rmq_unknown = _generate_status( + "focal", + "80.5/stable", + "ch:amd64/focal/rabbitmq-server-638", + [], + RMQ_UNITS, + RMQ_MACHINES, + "80.5", + ) + + return {"rabbitmq_server": mock_rmq, "unknown_rabbitmq_server": mock_rmq_unknown} - mock_keystone_focal_ussuri_victoria = _generate_status( + +def generate_ceph_mon_status(): + mock_ceph_mon_octopus = _generate_status( "focal", - "victoria/stable", - "ch:amd64/focal/keystone-638", + "octopus/stable", + "ch:amd64/focal/ceph-mon-178", + [], + CEPH_MON_UNITS, + CEPH_MON_MACHINES, + CEPH_WORKLOADS["octopus"], + ) + mock_ceph_mon_pacific = _generate_status( + "focal", + "pacific/stable", + "ch:amd64/focal/ceph-mon-178", [], - keystone_units, - keystone_machines, - keystone_workloads["ussuri-victoria"], + CEPH_MON_UNITS, + CEPH_MON_MACHINES, + CEPH_WORKLOADS["pacific"], ) + return {"ceph_mon_octopus": mock_ceph_mon_octopus, "ceph_mon_pacific": mock_ceph_mon_pacific} - mock_keystone_focal_wallaby = _generate_status( + +def generate_ceph_osd_status(): + mock_ceph_osd_octopus = _generate_status( "focal", - "wallaby/stable", - "ch:amd64/focal/keystone-638", + "octopus/stable", + "ch:amd64/focal/ceph-osd-177", [], - keystone_units, - keystone_machines, - keystone_workloads["wallaby"], + CEPH_OSD_UNITS, + CEPH_OSD_MACHINES, + CEPH_WORKLOADS["octopus"], ) + return {"ceph_osd_octopus": mock_ceph_osd_octopus} + +def generate_ovn_central_status(): + mock_ovn_central_20 = _generate_status( + "focal", + "20.03/stable", + "ch:amd64/focal/ovn-central-178", + [], + OVN_UNITS, + OVN_MACHINES, + OVN_WORKLOADS["20.03"], + ) + mock_ovn_central_22 = _generate_status( + "focal", + "22.03/stable", + "ch:amd64/focal/ovn-central-178", + [], + OVN_UNITS, + OVN_MACHINES, + OVN_WORKLOADS["22.03"], + ) + return {"ovn_central_20": mock_ovn_central_20, "ovn_central_22": mock_ovn_central_22} + + +def generate_mysql_innodb_cluster_status(): + mock_mysql_innodb_cluster = _generate_status( + "focal", + "8.0/stable", + "ch:amd64/focal/mysql-innodb-cluster-106", + [], + MYSQL_UNITS, + MYSQL_MACHINES, + MYSQL_WORKLOADS["8.0"], + ) + return {"mysql_innodb_cluster": mock_mysql_innodb_cluster} + + +def generate_glance_simplestreams_sync_status(): + mock_glance_simplestreams_sync_focal_ussuri = _generate_status( + "focal", + "ussuri/stable", + "ch:amd64/focal/glance-simplestreams-sync-78", + [], + GLANCE_SIMPLE_UNITS, + GLANCE_SIMPLE_MACHINES, + "", # there is no workload version for glance-simplestreams-sync + ) + return {"glance_simplestreams_sync_focal_ussuri": mock_glance_simplestreams_sync_focal_ussuri} + + +def generate_designate_bind_status(): + mock_designate_bind_focal_ussuri = _generate_status( + "focal", + "ussuri/stable", + "ch:amd64/focal/designate-bind-737", + [], + DESIGNATE_UNITS, + DESIGNATE_MACHINES, + DESIGNATE_WORKLOADS["ussuri"], + ) return { - "keystone_focal_ussuri": mock_keystone_focal_ussuri, - "keystone_bionic_ussuri": mock_keystone_bionic_ussuri, - "keystone_focal_ussuri_cs": mock_keystone_focal_ussuri_cs, - "keystone_focal_ussuri_victoria": mock_keystone_focal_ussuri_victoria, - "keystone_focal_victoria": mock_keystone_focal_victoria, - "keystone_focal_wallaby": mock_keystone_focal_wallaby, + "designate_bind_focal_ussuri": mock_designate_bind_focal_ussuri, + } + + +def generate_gnocchi_status(): + mock_gnocchi_focal_ussuri = _generate_status( + "focal", + "ussuri/stable", + "ch:amd64/focal/gnocchi-638", + [], + GNOCCHI_UNITS, + GNOCCHI_MACHINES, + GNOCCHI_WORKLOADS["ussuri"], + ) + mock_gnocchi_focal_xena = _generate_status( + "focal", + "xena/stable", + "ch:amd64/focal/gnocchi-638", + [], + GNOCCHI_UNITS, + GNOCCHI_MACHINES, + GNOCCHI_WORKLOADS["xena"], + ) + return { + "gnocchi_focal_ussuri": mock_gnocchi_focal_ussuri, + "gnocchi_focal_xena": mock_gnocchi_focal_xena, } +def generate_ovn_chassis_status(): + mock_ovn_chassis_focal_22 = _generate_status( + "focal", + "22.03/stable", + "ch:amd64/focal/ovn-chassis-178", + ["nova-compute"], + [], + [], + OVN_WORKLOADS["22.03"], + ) + mock_ovn_chassis_focal_20 = _generate_status( + "focal", + "20.03/stable", + "ch:amd64/focal/ovn-chassis-178", + ["nova-compute"], + [], + [], + OVN_WORKLOADS["20.03"], + ) + return { + "ovn_chassis_focal_20": mock_ovn_chassis_focal_20, + "ovn_chassis_focal_22": mock_ovn_chassis_focal_22, + } + + +def generate_keystone_ldap_status(): + mock_keystone_ldap_focal_ussuri = _generate_status( + "focal", + "ussuri/stable", + "ch:amd64/focal/keystone-ldap-437", + ["keystone"], + [], + [], + "", + ) + return {"keystone_ldap_focal_ussuri": mock_keystone_ldap_focal_ussuri} + + +def generate_ceph_dashboard_status(): + mock_ceph_dashboard_octopus = _generate_status( + "focal", + "octopus/stable", + "ch:amd64/focal/ceph-dashboard-178", + ["ceph-mon"], + [], + [], + CEPH_WORKLOADS["octopus"], + ) + mock_ceph_dashboard_pacific = _generate_status( + "focal", + "pacific/stable", + "ch:amd64/focal/ceph-dashboard-178", + ["ceph-mon"], + [], + [], + CEPH_WORKLOADS["pacific"], + ) + return { + "ceph_dashboard_octopus": mock_ceph_dashboard_octopus, + "ceph_dashboard_pacific": mock_ceph_dashboard_pacific, + } + + +def generate_mysql_router_status(): + mock_mysql_router = _generate_status( + "focal", "8.0/stable", "ch:amd64/focal/mysql-router-437", ["keystone"], [], [], "" + ) + return {"mysql_router": mock_mysql_router} + + +def generate_my_app(): + mock_my_app = _generate_status( + "focal", + "12.5/stable", + "ch:amd64/focal/my-app-638", + [], + ["my-app/0"], + ["0/lxd/11"], + "12.5", + ) + return {"my_app": mock_my_app} + + def _generate_status( - series, charm_channel, charm, subordinate_to, units, machines, workload_versions + series, charm_channel, charm, subordinate_to, units, machines, workload_version ): app_mock = MagicMock(spec_set=ApplicationStatus()) app_mock.series = series app_mock.charm_channel = charm_channel app_mock.charm = charm app_mock.subordinate_to = subordinate_to + # subordinates get workload version from the application + if subordinate_to: + app_mock.workload_version = workload_version - units_machines_workloads = zip(units, machines, workload_versions) + units_machines_workloads = zip_longest( + units, machines, [workload_version], fillvalue=workload_version + ) app_mock.units = _generate_units(units_machines_workloads) return app_mock @@ -463,12 +476,12 @@ def full_status(status, model): mock_full_status.model.name = model.name mock_full_status.applications = OrderedDict( [ - ("keystone", status["keystone_ussuri"]), - ("cinder", status["cinder_ussuri"]), + ("keystone", status["keystone_focal_ussuri"]), + ("cinder", status["cinder_focal_ussuri"]), ("rabbitmq-server", status["rabbitmq_server"]), - ("my_app", status["unknown_app"]), - ("nova-compute", status["nova_ussuri"]), - ("ceph-osd", status["ceph_osd_ussuri"]), + ("my_app", status["my_app"]), + ("nova-compute", status["nova_focal_ussuri"]), + ("ceph-osd", status["ceph_osd_octopus"]), ] ) return mock_full_status @@ -583,50 +596,76 @@ def model(config, apps_machines): @pytest.fixture -def apps(status, config, model): - keystone_ussuri_status = status["keystone_ussuri"] - keystone_wallaby_status = status["keystone_wallaby"] - cinder_ussuri_status = status["cinder_ussuri"] +def apps(status, config, model, apps_machines): + keystone_focal_ussuri_status = status["keystone_focal_ussuri"] + keystone_focal_wallaby_status = status["keystone_focal_wallaby"] + cinder_focal_ussuri_status = status["cinder_focal_ussuri"] rmq_status = status["rabbitmq_server"] - keystone_ldap_status = status["keystone-ldap"] - keystone_bionic_ussuri_status = status["keystone_bionic_ussuri"] + keystone_ldap_focal_ussuri_status = status["keystone_ldap_focal_ussuri"] keystone_ussuri = Keystone( - "keystone", keystone_ussuri_status, config["openstack_ussuri"], model, "keystone" + "keystone", + keystone_focal_ussuri_status, + config["openstack_ussuri"], + model, + "keystone", + apps_machines["keystone"], ) keystone_wallaby = Keystone( - "keystone", keystone_wallaby_status, config["openstack_wallaby"], model, "keystone" - ) - keystone_bionic_ussuri = OpenStackApplication( - "keystone", keystone_bionic_ussuri_status, config["openstack_ussuri"], model, "keystone" + "keystone", + keystone_focal_wallaby_status, + config["openstack_wallaby"], + model, + "keystone", + apps_machines["keystone"], ) cinder_ussuri = OpenStackApplication( - "cinder", cinder_ussuri_status, config["openstack_ussuri"], model, "cinder" + "cinder", + cinder_focal_ussuri_status, + config["openstack_ussuri"], + model, + "cinder", + apps_machines["cinder"], ) - rmq_ussuri = OpenStackAuxiliaryApplication( - "rabbitmq-server", rmq_status, config["auxiliary_ussuri"], model, "rabbitmq-server" + rmq = OpenStackAuxiliaryApplication( + "rabbitmq-server", + rmq_status, + config["auxiliary_ussuri"], + model, + "rabbitmq-server", + apps_machines["rmq"], ) rmq_wallaby = OpenStackAuxiliaryApplication( - "rabbitmq-server", rmq_status, config["auxiliary_wallaby"], model, "rabbitmq-server" + "rabbitmq-server", + rmq_status, + config["auxiliary_wallaby"], + model, + "rabbitmq-server", + apps_machines["rmq"], ) keystone_ldap = OpenStackSubordinateApplication( - "keystone-ldap", keystone_ldap_status, {}, model, "keystone-ldap" + "keystone-ldap", keystone_ldap_focal_ussuri_status, {}, model, "keystone-ldap", {} ) keystone_mysql_router = OpenStackAuxiliarySubordinateApplication( - "keystone-mysql-router", status["mysql_router"], {}, model, "mysql-router" + "keystone-mysql-router", status["mysql_router"], {}, model, "mysql-router", {} ) - nova_ussuri = OpenStackApplication( - "nova-compute", status["nova_ussuri"], config["openstack_ussuri"], model, "nova-compute" + nova_focal_ussuri = OpenStackApplication( + "nova-compute", + status["nova_focal_ussuri"], + config["openstack_ussuri"], + model, + "nova-compute", + apps_machines["nova-compute"], + apps_machines["nova-compute"], ) return { - "keystone_ussuri": keystone_ussuri, - "keystone_wallaby": keystone_wallaby, - "keystone_bionic_ussuri": keystone_bionic_ussuri, - "cinder_ussuri": cinder_ussuri, - "rmq_ussuri": rmq_ussuri, + "keystone_focal_ussuri": keystone_ussuri, + "keystone_focal_wallaby": keystone_wallaby, + "cinder_focal_ussuri": cinder_ussuri, + "rmq": rmq, "rmq_wallaby": rmq_wallaby, - "keystone_ldap": keystone_ldap, - "nova_ussuri": nova_ussuri, + "keystone_ldap_focal_ussuri": keystone_ldap, + "nova_focal_ussuri": nova_focal_ussuri, "keystone_mysql_router": keystone_mysql_router, } diff --git a/tests/unit/steps/test_steps_analyze.py b/tests/unit/steps/test_steps_analyze.py index 97c25e5c..d03530da 100644 --- a/tests/unit/steps/test_steps_analyze.py +++ b/tests/unit/steps/test_steps_analyze.py @@ -16,6 +16,7 @@ import pytest from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.apps.machine import Machine from cou.steps import analyze from cou.steps.analyze import Analysis @@ -75,9 +76,9 @@ def test_analysis_dump(apps, model): result = analyze.Analysis( model=model, apps_control_plane=[ - apps["keystone_ussuri"], - apps["cinder_ussuri"], - apps["rmq_ussuri"], + apps["keystone_focal_ussuri"], + apps["cinder_focal_ussuri"], + apps["rmq"], ], apps_data_plane=[], ) @@ -85,10 +86,16 @@ def test_analysis_dump(apps, model): @pytest.mark.asyncio -async def test_populate_model(full_status, config, model): +async def test_populate_model(full_status, config, model, apps_machines): model.get_status = AsyncMock(return_value=full_status) model.get_application_config = AsyncMock(return_value=config["openstack_ussuri"]) + machines = {} + for sub_dict in apps_machines.values(): + machines.update(sub_dict) + + model.get_model_machines = AsyncMock(return_value=machines) + # Initially, 6 applications are in the status: keystone, cinder, rabbitmq-server, my-app, # ceph-osd and nova-compute. my-app it's not on the lookup table, so won't be instantiated. assert len(full_status.applications) == 6 @@ -108,7 +115,7 @@ async def test_populate_model(full_status, config, model): @patch.object(analyze.Analysis, "_populate", new_callable=AsyncMock) async def test_analysis_create(mock_populate, apps, model): """Test analysis object.""" - exp_apps = [apps["keystone_ussuri"], apps["cinder_ussuri"], apps["rmq_ussuri"]] + exp_apps = [apps["keystone_focal_ussuri"], apps["cinder_focal_ussuri"], apps["rmq"]] expected_result = analyze.Analysis( model=model, apps_control_plane=exp_apps, apps_data_plane=[] ) @@ -123,7 +130,11 @@ async def test_analysis_create(mock_populate, apps, model): async def test_analysis_detect_current_cloud_os_release_different_releases(apps, model): result = analyze.Analysis( model=model, - apps_control_plane=[apps["rmq_ussuri"], apps["keystone_wallaby"], apps["cinder_ussuri"]], + apps_control_plane=[ + apps["rmq"], + apps["keystone_focal_wallaby"], + apps["cinder_focal_ussuri"], + ], apps_data_plane=[], ) @@ -135,7 +146,7 @@ async def test_analysis_detect_current_cloud_os_release_different_releases(apps, async def test_analysis_detect_current_cloud_os_release_same_release(apps, model): result = analyze.Analysis( model=model, - apps_control_plane=[apps["cinder_ussuri"], apps["keystone_ussuri"]], + apps_control_plane=[apps["cinder_focal_ussuri"], apps["keystone_focal_ussuri"]], apps_data_plane=[], ) @@ -147,7 +158,11 @@ async def test_analysis_detect_current_cloud_os_release_same_release(apps, model async def test_analysis_detect_current_cloud_series_same_series(apps, model): result = analyze.Analysis( model=model, - apps_control_plane=[apps["rmq_ussuri"], apps["keystone_wallaby"], apps["cinder_ussuri"]], + apps_control_plane=[ + apps["rmq"], + apps["keystone_focal_wallaby"], + apps["cinder_focal_ussuri"], + ], apps_data_plane=[], ) @@ -157,9 +172,13 @@ async def test_analysis_detect_current_cloud_series_same_series(apps, model): @pytest.mark.asyncio async def test_analysis_detect_current_cloud_series_different_series(apps, model): + # change keystone to bionic + keystone_bionic_ussuri = apps["keystone_focal_ussuri"] + keystone_bionic_ussuri.status.series = "bionic" + result = analyze.Analysis( model=model, - apps_control_plane=[apps["cinder_ussuri"], apps["keystone_bionic_ussuri"]], + apps_control_plane=[apps["cinder_focal_ussuri"], keystone_bionic_ussuri], apps_data_plane=[], ) @@ -174,9 +193,12 @@ def _app(name, units): return app -def _unit(machine): +def _unit(machine_id, is_data_plane): unit = MagicMock(spec_set=ApplicationUnit).return_value - unit.machine = machine + mock_machine = MagicMock(spec_set=Machine("1", "juju-efc45", "zone-1", False)) + mock_machine.machine_id = machine_id + mock_machine.is_data_plane = is_data_plane + unit.machine = mock_machine return unit @@ -184,22 +206,22 @@ def _unit(machine): "exp_control_plane, exp_data_plane", [ ( - [_app("keystone", [_unit("0"), _unit("1"), _unit("2")])], - [_app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")])], + [_app("keystone", [_unit("0", False), _unit("1", False), _unit("2", False)])], + [_app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)])], ), ( [], [ - _app("nova-compute", [_unit("0"), _unit("1"), _unit("2")]), - _app("keystone", [_unit("0"), _unit("1"), _unit("2")]), - _app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")]), + _app("nova-compute", [_unit("0", True), _unit("1", True), _unit("2", True)]), + _app("keystone", [_unit("0", False), _unit("1", False), _unit("2", False)]), + _app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)]), ], ), ( - [_app("keystone", [_unit("6"), _unit("7"), _unit("8")])], + [_app("keystone", [_unit("6", False), _unit("7", False), _unit("8", False)])], [ - _app("nova-compute", [_unit("0"), _unit("1"), _unit("2")]), - _app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")]), + _app("nova-compute", [_unit("0", True), _unit("1", True), _unit("2", True)]), + _app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)]), ], ), ], diff --git a/tests/unit/steps/test_steps_plan.py b/tests/unit/steps/test_steps_plan.py index 37743d0a..326de533 100644 --- a/tests/unit/steps/test_steps_plan.py +++ b/tests/unit/steps/test_steps_plan.py @@ -140,9 +140,9 @@ def generate_expected_upgrade_plan_subordinate(app, target, model): @pytest.mark.asyncio async def test_generate_plan(apps, model): target = OpenStackRelease("victoria") - app_keystone = apps["keystone_ussuri"] - app_cinder = apps["cinder_ussuri"] - app_keystone_ldap = apps["keystone_ldap"] + app_keystone = apps["keystone_focal_ussuri"] + app_cinder = apps["cinder_focal_ussuri"] + app_keystone_ldap = apps["keystone_ldap_focal_ussuri"] analysis_result = Analysis( model=model, apps_control_plane=[app_keystone, app_cinder, app_keystone_ldap], @@ -304,8 +304,8 @@ async def test_create_upgrade_plan_failed(): def test_plan_print_warn_manually_upgrade(mock_print, model, apps): result = Analysis( model=model, - apps_control_plane=[apps["keystone_wallaby"]], - apps_data_plane=[apps["nova_ussuri"]], + apps_control_plane=[apps["keystone_focal_wallaby"]], + apps_data_plane=[apps["nova_focal_ussuri"]], ) manually_upgrade_data_plane(result) mock_print.assert_called_with( @@ -317,8 +317,8 @@ def test_plan_print_warn_manually_upgrade(mock_print, model, apps): def test_analysis_not_print_warn_manually_upgrade(mock_print, model, apps): result = Analysis( model=model, - apps_control_plane=[apps["keystone_ussuri"]], - apps_data_plane=[apps["nova_ussuri"]], + apps_control_plane=[apps["keystone_focal_ussuri"]], + apps_data_plane=[apps["nova_focal_ussuri"]], ) manually_upgrade_data_plane(result) mock_print.assert_not_called() From 7f8aa741d2cf4e92dcfb0dd26ab4ab8d5b0febb3 Mon Sep 17 00:00:00 2001 From: Gabriel Cocenza Date: Wed, 24 Jan 2024 19:31:44 -0300 Subject: [PATCH 3/6] - removed is_data_plane from machine - channel_codename property started failing coverage and we don't need to make another check because channel is already checked. - finished unit tests --- cou/apps/auxiliary.py | 2 +- cou/apps/base.py | 12 +---- cou/apps/machine.py | 1 - cou/steps/analyze.py | 33 ++++++++----- cou/utils/juju_utils.py | 37 ++++++--------- tests/unit/apps/test_machines.py | 29 ++++-------- tests/unit/conftest.py | 36 +++++++-------- tests/unit/steps/test_steps_analyze.py | 23 ++++----- tests/unit/utils/test_juju_utils.py | 64 +++++++++++++++++++++++++- 9 files changed, 137 insertions(+), 100 deletions(-) diff --git a/cou/apps/auxiliary.py b/cou/apps/auxiliary.py index 423d07cd..8c0f9290 100644 --- a/cou/apps/auxiliary.py +++ b/cou/apps/auxiliary.py @@ -182,7 +182,7 @@ def pre_upgrade_steps(self, target: OpenStackRelease) -> list[PreUpgradeStep]: return super().pre_upgrade_steps(target) -@AppFactory.register_application(["mysql_innodb_cluster"]) +@AppFactory.register_application(["mysql-innodb-cluster"]) class MysqlInnodbClusterApplication(OpenStackAuxiliaryApplication): """Application for mysql-innodb-cluster charm.""" diff --git a/cou/apps/base.py b/cou/apps/base.py index 64dfca1f..8758e21f 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -373,16 +373,8 @@ def channel_codename(self) -> OpenStackRelease: :return: OpenStackRelease object :rtype: OpenStackRelease """ - try: - # get the OpenStack release from the channel track of the application. - os_track_release_channel = OpenStackRelease(self._get_track_from_channel(self.channel)) - except ValueError: - logger.debug( - "The current channel of '%s' does not exist or is unexpectedly formatted", - self.name, - ) - os_track_release_channel = self.current_os_release - return os_track_release_channel + # get the OpenStack release from the channel track of the application. + return OpenStackRelease(self._get_track_from_channel(self.channel)) @property def can_upgrade_current_channel(self) -> bool: diff --git a/cou/apps/machine.py b/cou/apps/machine.py index 5f99cedc..04d6e4e4 100644 --- a/cou/apps/machine.py +++ b/cou/apps/machine.py @@ -25,7 +25,6 @@ class Machine: machine_id: str hostname: str az: Optional[str] # simple deployments may not have azs - is_data_plane: bool def __repr__(self) -> str: """Representation of the juju Machine. diff --git a/cou/steps/analyze.py b/cou/steps/analyze.py index 633d769b..70278145 100644 --- a/cou/steps/analyze.py +++ b/cou/steps/analyze.py @@ -22,7 +22,7 @@ from cou.apps.base import OpenStackApplication from cou.apps.factory import AppFactory from cou.utils import juju_utils -from cou.utils.openstack import UPGRADE_ORDER, OpenStackRelease +from cou.utils.openstack import DATA_PLANE_CHARMS, UPGRADE_ORDER, OpenStackRelease logger = logging.getLogger(__name__) @@ -39,8 +39,6 @@ class Analysis: :type apps_data_plane: list[OpenStackApplication] """ - # pylint: disable=too-many-instance-attributes - model: juju_utils.COUModel apps_control_plane: list[OpenStackApplication] apps_data_plane: list[OpenStackApplication] @@ -68,9 +66,25 @@ def _split_apps( :return: Control plane and data plane application lists. :rtype: tuple[list[OpenStackApplication], list[OpenStackApplication]] """ + + def is_data_plane(app: OpenStackApplication) -> bool: + """Check if app belong to data plane. + + :param app: application + :type app: OpenStackApplication + :return: boolean + :rtype: bool + """ + return app.charm in DATA_PLANE_CHARMS + control_plane, data_plane = [], [] + data_plane_machines = { + unit.machine for app in apps if is_data_plane(app) for unit in app.units + } for app in apps: - if any(unit.machine.is_data_plane for unit in app.units): + if is_data_plane(app): + data_plane.append(app) + elif any(unit.machine in data_plane_machines for unit in app.units): data_plane.append(app) else: control_plane.append(app) @@ -91,11 +105,7 @@ async def create(cls, model: juju_utils.COUModel) -> Analysis: control_plane, data_plane = cls._split_apps(apps) - return Analysis( - model=model, - apps_data_plane=data_plane, - apps_control_plane=control_plane, - ) + return Analysis(model=model, apps_data_plane=data_plane, apps_control_plane=control_plane) @classmethod async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplication]: @@ -111,15 +121,14 @@ async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplicatio :rtype: List[OpenStackApplication] """ juju_status = await model.get_status() - juju_applications = await model.get_applications() juju_machines = await model.get_model_machines() apps = { AppFactory.create( name=app, status=app_status, - config=juju_applications[app].get_config(), + config=await model.get_application_config(app), model=model, - charm=juju_applications[app].charm_name, + charm=await model.get_charm_name(app), machines={ unit_status.machine: juju_machines[unit_status.machine] for unit_status in app_status.units.values() diff --git a/cou/utils/juju_utils.py b/cou/utils/juju_utils.py index f2f023ef..b5bc9254 100644 --- a/cou/utils/juju_utils.py +++ b/cou/utils/juju_utils.py @@ -25,6 +25,7 @@ from juju.client.connector import NoConnectionException from juju.client.jujudata import FileJujuData from juju.errors import JujuAppError, JujuError, JujuUnitError +from juju.machine import Machine as JujuMachine from juju.model import Model from juju.unit import Unit from macaroonbakery.httpbakery import BakeryException @@ -40,7 +41,7 @@ UnitNotFound, WaitForApplicationsTimeout, ) -from cou.utils.openstack import DATA_PLANE_CHARMS, is_charm_supported +from cou.utils.openstack import is_charm_supported JUJU_MAX_FRAME_SIZE: int = 2**30 DEFAULT_TIMEOUT: int = int(os.environ.get("COU_TIMEOUT", 10)) @@ -191,14 +192,14 @@ async def _get_action_result(self, action: Action, raise_on_failure: bool) -> Ac return result - async def get_applications(self) -> dict[str, Application]: - """Get all applications from the model. + async def _get_machines(self) -> dict[str, JujuMachine]: + """Get all machines from the model. - :return: Applications from the model connected. + :return: Machines from the model connected. :rtype: dict[str, Application] """ model = await self._get_model() - return model.applications + return model.machines async def _get_application(self, name: str) -> Application: """Get juju.application.Application from model. @@ -516,20 +517,12 @@ async def get_model_machines(self) -> dict[str, Machine]: :return: Dictionary of the machines found in the model. E.g: {'0': Machine0} :rtype: dict[str, Machine] """ - juju_applications = await self.get_applications() - machines: dict[str, Machine] = {} - for app in juju_applications.values(): - charm_name = app.charm_name - for unit in app.units: - unit_machine = unit.machine.data - machine = machines.get( - unit_machine["id"], - Machine( - machine_id=unit_machine["id"], - hostname=unit_machine["hostname"], - az=unit_machine["hardware-characteristic"].get("availability-zone"), - is_data_plane=charm_name in DATA_PLANE_CHARMS, - ), - ) - machines[unit_machine["id"]] = machine - return machines + juju_machines = await self._get_machines() + return { + machine.id: Machine( + machine_id=machine.data["id"], + hostname=machine.data["hostname"], + az=machine.data["hardware-characteristics"].get("availability-zone"), + ) + for machine in juju_machines.values() + } diff --git a/tests/unit/apps/test_machines.py b/tests/unit/apps/test_machines.py index 8d36e6b6..a98c4bc8 100644 --- a/tests/unit/apps/test_machines.py +++ b/tests/unit/apps/test_machines.py @@ -17,40 +17,29 @@ @pytest.mark.parametrize( - "machine_id, hostname, az, is_data_plane", + "machine_id, hostname, az", [ # one field different is considered another machine - ("0", "juju-c307f8-my_model-0", "zone-1", True), - ("1", "juju-c307f8-my_model-1", "zone-2", False), + ("0", "juju-c307f8-my_model-0", "zone-3"), + ("1", "juju-c307f8-my_model-1", "zone-2"), ], ) -def test_machine_not_eq(machine_id, hostname, az, is_data_plane): - machine_0 = Machine( - machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False - ) - - machine_1 = Machine( - machine_id=machine_id, hostname=hostname, az=az, is_data_plane=is_data_plane - ) +def test_machine_not_eq(machine_id, hostname, az): + machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") + machine_1 = Machine(machine_id=machine_id, hostname=hostname, az=az) assert machine_0 != machine_1 def test_machine_eq(): - machine_0 = Machine( - machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False - ) + machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") - machine_1 = Machine( - machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False - ) + machine_1 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") assert machine_0 == machine_1 def test_machine_repr(): - machine_0 = Machine( - machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1", is_data_plane=False - ) + machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") expected_repr = "Machine[0]" assert repr(machine_0) == expected_repr diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index b9d21a2f..ce434f46 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -104,34 +104,30 @@ def _generate_units(units_machines_workloads): @pytest.fixture def apps_machines(): return { - **_generate_apps_machines("keystone", KEYSTONE_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("cinder", CINDER_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("nova-compute", NOVA_MACHINES, STANDARD_AZS, True), - **_generate_apps_machines("rmq", RMQ_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("ceph-mon", CEPH_MON_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("ovn-central", OVN_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("mysql-innodb-cluster", MYSQL_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("keystone", KEYSTONE_MACHINES, STANDARD_AZS), + **_generate_apps_machines("cinder", CINDER_MACHINES, STANDARD_AZS), + **_generate_apps_machines("nova-compute", NOVA_MACHINES, STANDARD_AZS), + **_generate_apps_machines("rmq", RMQ_MACHINES, STANDARD_AZS), + **_generate_apps_machines("ceph-mon", CEPH_MON_MACHINES, STANDARD_AZS), + **_generate_apps_machines("ovn-central", OVN_MACHINES, STANDARD_AZS), + **_generate_apps_machines("mysql-innodb-cluster", MYSQL_MACHINES, STANDARD_AZS), **_generate_apps_machines( - "glance-simplestreams-sync", GLANCE_SIMPLE_MACHINES, STANDARD_AZS, False + "glance-simplestreams-sync", GLANCE_SIMPLE_MACHINES, STANDARD_AZS ), - **_generate_apps_machines("gnocchi", GNOCCHI_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("designate-bind", DESIGNATE_MACHINES, STANDARD_AZS, False), - **_generate_apps_machines("ceph-osd", CEPH_OSD_MACHINES, STANDARD_AZS, True), - **_generate_apps_machines("my-app", MY_APP_MACHINES, STANDARD_AZS, False), + **_generate_apps_machines("gnocchi", GNOCCHI_MACHINES, STANDARD_AZS), + **_generate_apps_machines("designate-bind", DESIGNATE_MACHINES, STANDARD_AZS), + **_generate_apps_machines("ceph-osd", CEPH_OSD_MACHINES, STANDARD_AZS), + **_generate_apps_machines("my-app", MY_APP_MACHINES, STANDARD_AZS), } -def _generate_apps_machines(charm, machines, azs, is_data_plane): +def _generate_apps_machines(charm, machines, azs): hostnames = [f"{HOSTNAME_PREFIX}-{machine}" for machine in machines] - machines_hostnames_azs_is_data_plane = zip_longest( - machines, hostnames, azs, [is_data_plane], fillvalue=is_data_plane - ) + machines_hostnames_azs = zip(machines, hostnames, azs) return { charm: { - machine_id: Machine( - machine_id=machine_id, hostname=hostname, az=az, is_data_plane=is_data_plane - ) - for machine_id, hostname, az, is_data_plane in machines_hostnames_azs_is_data_plane + machine_id: Machine(machine_id=machine_id, hostname=hostname, az=az) + for machine_id, hostname, az in machines_hostnames_azs } } diff --git a/tests/unit/steps/test_steps_analyze.py b/tests/unit/steps/test_steps_analyze.py index d03530da..f140e19e 100644 --- a/tests/unit/steps/test_steps_analyze.py +++ b/tests/unit/steps/test_steps_analyze.py @@ -193,12 +193,9 @@ def _app(name, units): return app -def _unit(machine_id, is_data_plane): +def _unit(machine_id): unit = MagicMock(spec_set=ApplicationUnit).return_value - mock_machine = MagicMock(spec_set=Machine("1", "juju-efc45", "zone-1", False)) - mock_machine.machine_id = machine_id - mock_machine.is_data_plane = is_data_plane - unit.machine = mock_machine + unit.machine = Machine(machine_id, "juju-efc45", "zone-1") return unit @@ -206,22 +203,22 @@ def _unit(machine_id, is_data_plane): "exp_control_plane, exp_data_plane", [ ( - [_app("keystone", [_unit("0", False), _unit("1", False), _unit("2", False)])], - [_app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)])], + [_app("keystone", [_unit("0"), _unit("1"), _unit("2")])], + [_app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")])], ), ( [], [ - _app("nova-compute", [_unit("0", True), _unit("1", True), _unit("2", True)]), - _app("keystone", [_unit("0", False), _unit("1", False), _unit("2", False)]), - _app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)]), + _app("nova-compute", [_unit("0"), _unit("1"), _unit("2")]), + _app("keystone", [_unit("0"), _unit("1"), _unit("2")]), + _app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")]), ], ), ( - [_app("keystone", [_unit("6", False), _unit("7", False), _unit("8", False)])], + [_app("keystone", [_unit("6"), _unit("7"), _unit("8")])], [ - _app("nova-compute", [_unit("0", True), _unit("1", True), _unit("2", True)]), - _app("ceph-osd", [_unit("3", True), _unit("4", True), _unit("5", True)]), + _app("nova-compute", [_unit("0"), _unit("1"), _unit("2")]), + _app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")]), ], ), ], diff --git a/tests/unit/utils/test_juju_utils.py b/tests/unit/utils/test_juju_utils.py index 2bcfb742..04dada62 100644 --- a/tests/unit/utils/test_juju_utils.py +++ b/tests/unit/utils/test_juju_utils.py @@ -12,15 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch import pytest from juju.action import Action from juju.application import Application from juju.client.connector import NoConnectionException +from juju.machine import Machine as JujuMachine from juju.model import Model from juju.unit import Unit +from cou.apps.machine import Machine from cou.exceptions import ( ActionFailed, ApplicationError, @@ -226,6 +228,18 @@ async def test_coumodel_connect(mocked_model): ) +@pytest.mark.asyncio +async def test_coumodel_get_machines(mocked_model): + """Test COUModel get machines.""" + model = juju_utils.COUModel("test-model") + juju_machines = PropertyMock() + type(mocked_model).machines = juju_machines + + await model._get_machines() + + juju_machines.assert_called_once() + + @pytest.mark.asyncio async def test_coumodel_get_application(mocked_model): """Test COUModel get application.""" @@ -530,3 +544,51 @@ async def test_coumodel_wait_for_active_idle_timeout(mock_get_supported_apps, mo status="active", ) mock_get_supported_apps.assert_not_awaited() + + +@pytest.mark.parametrize( + "machines_ids, expected_machines", + [ + ( + [0, 1, 2], + { + "0": Machine("0", "juju-90e5ce-0", "zone-1"), + "1": Machine( + "1", + "juju-90e5ce-1", + "zone-2", + ), + "2": Machine("2", "juju-90e5ce-2", "zone-3"), + }, + ), + ], +) +@pytest.mark.asyncio +@patch("cou.utils.juju_utils.COUModel._get_machines") +async def test_get_model_machines(mock_model_machines, machines_ids, expected_machines): + """Test COUModel generate the Machines class.""" + juju_machines = _generate_juju_machines_mock(machines_ids) + + mock_model_machines.return_value = juju_machines + model = juju_utils.COUModel(None) + machines = await model.get_model_machines() + assert machines == expected_machines + + +def _generate_juju_machines_mock(machine_ids): + juju_machines = {} + for machine_id in machine_ids: + machine = MagicMock(spec=JujuMachine) + machine.id = str(machine_id) + machine.data = { + "id": str(machine_id), + "hardware-characteristics": { + "arch": "amd64", + "mem": 0, + "cpu-cores": 0, + "availability-zone": f"zone-{str(machine_id + 1)}", + }, + "hostname": f"juju-90e5ce-{str(machine_id)}", + } + juju_machines.update({machine_id: machine}) + return juju_machines From 849017f2ecf43baa4d6c44f5a37c9d4932b54cb1 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 12 Mar 2024 17:58:44 +0100 Subject: [PATCH 4/6] fix merge conflicts with main --- cou/apps/base.py | 9 +- cou/apps/factory.py | 10 +- cou/apps/machine.py | 35 ------ cou/steps/analyze.py | 2 +- cou/utils/juju_utils.py | 40 ++++--- tests/unit/apps/test_machines.py | 45 -------- tests/unit/conftest.py | 15 +-- tests/unit/steps/test_steps_analyze.py | 6 +- tests/unit/utils/test_juju_utils.py | 145 ++++++++++++++----------- 9 files changed, 119 insertions(+), 188 deletions(-) delete mode 100644 cou/apps/machine.py delete mode 100644 tests/unit/apps/test_machines.py diff --git a/cou/apps/base.py b/cou/apps/base.py index 8758e21f..1486bc69 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -24,7 +24,6 @@ from juju.client._definitions import ApplicationStatus, UnitStatus from ruamel.yaml import YAML -from cou.apps.machine import Machine from cou.exceptions import ( ApplicationError, HaltUpgradePlanGeneration, @@ -38,7 +37,7 @@ UpgradeStep, ) from cou.utils.app_utils import upgrade_packages -from cou.utils.juju_utils import COUModel +from cou.utils.juju_utils import COUMachine, COUModel from cou.utils.openstack import ( DISTRO_TO_OPENSTACK_MAPPING, OpenStackCodenameLookup, @@ -56,7 +55,7 @@ class ApplicationUnit: name: str os_version: OpenStackRelease - machine: Machine + machine: COUMachine workload_version: str = "" @@ -74,6 +73,8 @@ class OpenStackApplication: :type model: COUModel :param charm: Name of the charm. :type charm: str + :param machines: dictionary with COUMachine + :type machines: dict[str, COUMachine] :param charm_origin: Origin of the charm (local, ch, cs and etc.), defaults to "" :type charm_origin: str, defaults to "" :param os_origin: OpenStack origin of the application. E.g: cloud:focal-wallaby, defaults to "" @@ -101,7 +102,7 @@ class OpenStackApplication: config: dict model: COUModel charm: str - machines: dict[str, Machine] + machines: dict[str, COUMachine] charm_origin: str = "" os_origin: str = "" origin_setting: Optional[str] = None diff --git a/cou/apps/factory.py b/cou/apps/factory.py index e57a4afd..82abf027 100644 --- a/cou/apps/factory.py +++ b/cou/apps/factory.py @@ -21,8 +21,8 @@ from juju.client._definitions import ApplicationStatus -from cou.apps.base import Machine, OpenStackApplication -from cou.utils.juju_utils import COUModel +from cou.apps.base import OpenStackApplication +from cou.utils.juju_utils import COUMachine, COUModel from cou.utils.openstack import is_charm_supported logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def create( config: dict, model: COUModel, charm: str, - machines: dict[str, Machine], + machines: dict[str, COUMachine], ) -> Optional[OpenStackApplication]: """Create the OpenStackApplication or registered subclasses. @@ -49,8 +49,8 @@ def create( decorator can be instantiated and used with their customized methods. :param name: Name of the application :type name: str - :param machines: Machines in the model - :type machines: dict[str, Machine] + :param machines: COUMachine in the model + :type machines: dict[str, COUMachine] :param status: Status of the application :type status: ApplicationStatus :param config: Configuration of the application diff --git a/cou/apps/machine.py b/cou/apps/machine.py deleted file mode 100644 index 04d6e4e4..00000000 --- a/cou/apps/machine.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2023 Canonical Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Machine class.""" - -from dataclasses import dataclass -from typing import Optional - - -@dataclass(frozen=True) -class Machine: - """Representation of a juju machine.""" - - machine_id: str - hostname: str - az: Optional[str] # simple deployments may not have azs - - def __repr__(self) -> str: - """Representation of the juju Machine. - - :return: Representation of the juju Machine - :rtype: str - """ - return f"Machine[{self.machine_id}]" diff --git a/cou/steps/analyze.py b/cou/steps/analyze.py index 70278145..ccb0e99e 100644 --- a/cou/steps/analyze.py +++ b/cou/steps/analyze.py @@ -121,7 +121,7 @@ async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplicatio :rtype: List[OpenStackApplication] """ juju_status = await model.get_status() - juju_machines = await model.get_model_machines() + juju_machines = await model.get_machines() apps = { AppFactory.create( name=app, diff --git a/cou/utils/juju_utils.py b/cou/utils/juju_utils.py index b5bc9254..a2200a5a 100644 --- a/cou/utils/juju_utils.py +++ b/cou/utils/juju_utils.py @@ -16,6 +16,7 @@ import asyncio import logging import os +from dataclasses import dataclass from datetime import datetime from typing import Any, Callable, Optional @@ -25,13 +26,11 @@ from juju.client.connector import NoConnectionException from juju.client.jujudata import FileJujuData from juju.errors import JujuAppError, JujuError, JujuUnitError -from juju.machine import Machine as JujuMachine from juju.model import Model from juju.unit import Unit from macaroonbakery.httpbakery import BakeryException from six import wraps -from cou.apps.machine import Machine from cou.exceptions import ( ActionFailed, ApplicationError, @@ -131,6 +130,15 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: # pylint: disable=W9011 return _wrapper +@dataclass(frozen=True) +class COUMachine: + """Representation of a juju machine.""" + + machine_id: str + apps: tuple[str] + az: Optional[str] = None # simple deployments may not have azs + + class COUModel: """COU model object. @@ -192,15 +200,6 @@ async def _get_action_result(self, action: Action, raise_on_failure: bool) -> Ac return result - async def _get_machines(self) -> dict[str, JujuMachine]: - """Get all machines from the model. - - :return: Machines from the model connected. - :rtype: dict[str, Application] - """ - model = await self._get_model() - return model.machines - async def _get_application(self, name: str) -> Application: """Get juju.application.Application from model. @@ -511,18 +510,23 @@ async def _wait_for_active_idle() -> None: await _wait_for_active_idle() - async def get_model_machines(self) -> dict[str, Machine]: + 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] """ - juju_machines = await self._get_machines() + model = await self._get_model() + return { - machine.id: Machine( - machine_id=machine.data["id"], - hostname=machine.data["hostname"], - az=machine.data["hardware-characteristics"].get("availability-zone"), + 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 juju_machines.values() + for machine in model.machines.values() } diff --git a/tests/unit/apps/test_machines.py b/tests/unit/apps/test_machines.py deleted file mode 100644 index a98c4bc8..00000000 --- a/tests/unit/apps/test_machines.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2023 Canonical Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pytest - -from cou.apps.machine import Machine - - -@pytest.mark.parametrize( - "machine_id, hostname, az", - [ - # one field different is considered another machine - ("0", "juju-c307f8-my_model-0", "zone-3"), - ("1", "juju-c307f8-my_model-1", "zone-2"), - ], -) -def test_machine_not_eq(machine_id, hostname, az): - machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") - machine_1 = Machine(machine_id=machine_id, hostname=hostname, az=az) - - assert machine_0 != machine_1 - - -def test_machine_eq(): - machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") - - machine_1 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") - - assert machine_0 == machine_1 - - -def test_machine_repr(): - machine_0 = Machine(machine_id="0", hostname="juju-c307f8-my_model-0", az="zone-1") - expected_repr = "Machine[0]" - assert repr(machine_0) == expected_repr diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index ce434f46..c86f271a 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -25,12 +25,11 @@ from cou.apps.auxiliary_subordinate import OpenStackAuxiliarySubordinateApplication from cou.apps.base import ApplicationUnit, OpenStackApplication from cou.apps.core import Keystone -from cou.apps.machine import Machine from cou.apps.subordinate import OpenStackSubordinateApplication +from cou.utils.juju_utils import COUMachine from cou.utils.openstack import OpenStackRelease STANDARD_AZS = ["zone-1", "zone-2", "zone-3"] -HOSTNAME_PREFIX = "juju-c307f8" KEYSTONE_UNITS = ["keystone/0", "keystone/1", "keystone/2"] KEYSTONE_MACHINES = ["0/lxd/12", "1/lxd/12", "2/lxd/13"] @@ -122,14 +121,8 @@ def apps_machines(): def _generate_apps_machines(charm, machines, azs): - hostnames = [f"{HOSTNAME_PREFIX}-{machine}" for machine in machines] - machines_hostnames_azs = zip(machines, hostnames, azs) - return { - charm: { - machine_id: Machine(machine_id=machine_id, hostname=hostname, az=az) - for machine_id, hostname, az in machines_hostnames_azs - } - } + machines_azs = zip(machines, azs) + return {charm: {machine_id: COUMachine(machine_id, (), az) for machine_id, az in machines_azs}} @pytest.fixture @@ -586,7 +579,7 @@ def model(config, apps_machines): model.scp_from_unit = AsyncMock() model.get_application_config = mock_get_app_config = AsyncMock() mock_get_app_config.side_effect = config.get - model.get_model_machines = machines + model.get_machines = machines return model diff --git a/tests/unit/steps/test_steps_analyze.py b/tests/unit/steps/test_steps_analyze.py index f140e19e..10f587a2 100644 --- a/tests/unit/steps/test_steps_analyze.py +++ b/tests/unit/steps/test_steps_analyze.py @@ -16,9 +16,9 @@ import pytest from cou.apps.base import ApplicationUnit, OpenStackApplication -from cou.apps.machine import Machine from cou.steps import analyze from cou.steps.analyze import Analysis +from cou.utils.juju_utils import COUMachine def test_analysis_dump(apps, model): @@ -94,7 +94,7 @@ async def test_populate_model(full_status, config, model, apps_machines): for sub_dict in apps_machines.values(): machines.update(sub_dict) - model.get_model_machines = AsyncMock(return_value=machines) + model.get_machines = AsyncMock(return_value=machines) # Initially, 6 applications are in the status: keystone, cinder, rabbitmq-server, my-app, # ceph-osd and nova-compute. my-app it's not on the lookup table, so won't be instantiated. @@ -195,7 +195,7 @@ def _app(name, units): def _unit(machine_id): unit = MagicMock(spec_set=ApplicationUnit).return_value - unit.machine = Machine(machine_id, "juju-efc45", "zone-1") + unit.machine = COUMachine(machine_id, (), "zone-1") return unit diff --git a/tests/unit/utils/test_juju_utils.py b/tests/unit/utils/test_juju_utils.py index 04dada62..9a79672f 100644 --- a/tests/unit/utils/test_juju_utils.py +++ b/tests/unit/utils/test_juju_utils.py @@ -12,17 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio -from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch +from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from juju.action import Action from juju.application import Application from juju.client.connector import NoConnectionException -from juju.machine import Machine as JujuMachine +from juju.machine import Machine from juju.model import Model from juju.unit import Unit -from cou.apps.machine import Machine from cou.exceptions import ( ActionFailed, ApplicationError, @@ -35,6 +34,35 @@ from cou.utils import juju_utils +@pytest.fixture +def mocked_model(mocker): + """Fixture providing mocked juju.model.Model object.""" + mocker.patch("cou.utils.juju_utils.FileJujuData") + model_mocker = mocker.patch("cou.utils.juju_utils.Model", return_value=AsyncMock(Model)) + model = model_mocker.return_value + model.connection.return_value.is_open = True # simulate already connected model + yield model + + +def _generate_juju_unit(app: str, machine_id: str) -> MagicMock: + unit = MagicMock(set=Unit)() + unit.application = app + unit.machine.id = machine_id + return unit + + +def _generate_juju_machine(machine_id: str) -> MagicMock: + machine = MagicMock(set=Machine)() + machine.id = machine_id + machine.hardware_characteristics = { + "arch": "amd64", + "mem": 0, + "cpu-cores": 0, + "availability-zone": f"zone-{int(machine_id) + 1}", + } + return machine + + def test_normalize_action_results(): results = {"Stderr": "error", "stdout": "output"} expected = {"Stderr": "error", "Stdout": "output", "stderr": "error", "stdout": "output"} @@ -137,14 +165,26 @@ async def func(self): await test_model.func() -@pytest.fixture -def mocked_model(mocker): - """Fixture providing mocked juju.model.Model object.""" - mocker.patch("cou.utils.juju_utils.FileJujuData") - model_mocker = mocker.patch("cou.utils.juju_utils.Model", return_value=AsyncMock(Model)) - model = model_mocker.return_value - model.connection.return_value.is_open = True # simulate already connected model - yield model +@pytest.mark.parametrize( + "machine_id, az", + [ + # one field different is considered another machine + ("0", "zone-3"), + ("1", "zone-2"), + ], +) +def test_machine_not_eq(machine_id, az): + machine_0 = juju_utils.COUMachine("0", (), "zone-1") + machine_1 = juju_utils.COUMachine(machine_id, (), az) + + assert machine_0 != machine_1 + + +def test_machine_eq(): + machine_0 = juju_utils.COUMachine("0", (), "zone-1") + machine_1 = juju_utils.COUMachine("0", (), "zone-1") + + assert machine_0 == machine_1 @patch("cou.utils.juju_utils.FileJujuData") @@ -228,18 +268,6 @@ async def test_coumodel_connect(mocked_model): ) -@pytest.mark.asyncio -async def test_coumodel_get_machines(mocked_model): - """Test COUModel get machines.""" - model = juju_utils.COUModel("test-model") - juju_machines = PropertyMock() - type(mocked_model).machines = juju_machines - - await model._get_machines() - - juju_machines.assert_called_once() - - @pytest.mark.asyncio async def test_coumodel_get_application(mocked_model): """Test COUModel get application.""" @@ -546,49 +574,34 @@ async def test_coumodel_wait_for_active_idle_timeout(mock_get_supported_apps, mo mock_get_supported_apps.assert_not_awaited() -@pytest.mark.parametrize( - "machines_ids, expected_machines", - [ - ( - [0, 1, 2], - { - "0": Machine("0", "juju-90e5ce-0", "zone-1"), - "1": Machine( - "1", - "juju-90e5ce-1", - "zone-2", - ), - "2": Machine("2", "juju-90e5ce-2", "zone-3"), - }, - ), - ], -) @pytest.mark.asyncio -@patch("cou.utils.juju_utils.COUModel._get_machines") -async def test_get_model_machines(mock_model_machines, machines_ids, expected_machines): - """Test COUModel generate the Machines class.""" - juju_machines = _generate_juju_machines_mock(machines_ids) - - mock_model_machines.return_value = juju_machines - model = juju_utils.COUModel(None) - machines = await model.get_model_machines() - assert machines == expected_machines +async def test_get_machines(mocked_model): + """Test COUModel getting machines from model.""" + expected_machines = { + "0": juju_utils.COUMachine( + "0", + ( + "app1", + "app2", + ), + "zone-1", + ), + "1": juju_utils.COUMachine("1", ("app1",), "zone-2"), + "2": juju_utils.COUMachine("2", ("app1",), "zone-3"), + } + mocked_model.machines = {f"{i}": _generate_juju_machine(f"{i}") for i in range(3)} + mocked_model.units = { + "app1/0": _generate_juju_unit("app1", "0"), + "app1/1": _generate_juju_unit("app1", "1"), + "app1/2": _generate_juju_unit("app1", "2"), + "app2/0": _generate_juju_unit("app2", "0"), + } + mocked_model.applications = { + "app1": MagicMock(spec_set=Application)(), + "app2": MagicMock(spec_set=Application)(), + } + model = juju_utils.COUModel("test-model") + machines = await model.get_machines() -def _generate_juju_machines_mock(machine_ids): - juju_machines = {} - for machine_id in machine_ids: - machine = MagicMock(spec=JujuMachine) - machine.id = str(machine_id) - machine.data = { - "id": str(machine_id), - "hardware-characteristics": { - "arch": "amd64", - "mem": 0, - "cpu-cores": 0, - "availability-zone": f"zone-{str(machine_id + 1)}", - }, - "hostname": f"juju-90e5ce-{str(machine_id)}", - } - juju_machines.update({machine_id: machine}) - return juju_machines + assert machines == expected_machines From a54c1793e8c0e44b35b3162d41b8b1c4d2ee38c4 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 12 Mar 2024 18:02:02 +0100 Subject: [PATCH 5/6] rename COUMachine to Machine --- cou/apps/base.py | 10 +++++----- cou/apps/factory.py | 8 ++++---- cou/utils/juju_utils.py | 6 +++--- tests/unit/conftest.py | 4 ++-- tests/unit/steps/test_steps_analyze.py | 4 ++-- tests/unit/utils/test_juju_utils.py | 14 +++++++------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cou/apps/base.py b/cou/apps/base.py index 1486bc69..c2b02b13 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -37,7 +37,7 @@ UpgradeStep, ) from cou.utils.app_utils import upgrade_packages -from cou.utils.juju_utils import COUMachine, COUModel +from cou.utils.juju_utils import COUModel, Machine from cou.utils.openstack import ( DISTRO_TO_OPENSTACK_MAPPING, OpenStackCodenameLookup, @@ -55,7 +55,7 @@ class ApplicationUnit: name: str os_version: OpenStackRelease - machine: COUMachine + machine: Machine workload_version: str = "" @@ -73,8 +73,8 @@ class OpenStackApplication: :type model: COUModel :param charm: Name of the charm. :type charm: str - :param machines: dictionary with COUMachine - :type machines: dict[str, COUMachine] + :param machines: dictionary with Machine + :type machines: dict[str, Machine] :param charm_origin: Origin of the charm (local, ch, cs and etc.), defaults to "" :type charm_origin: str, defaults to "" :param os_origin: OpenStack origin of the application. E.g: cloud:focal-wallaby, defaults to "" @@ -102,7 +102,7 @@ class OpenStackApplication: config: dict model: COUModel charm: str - machines: dict[str, COUMachine] + machines: dict[str, Machine] charm_origin: str = "" os_origin: str = "" origin_setting: Optional[str] = None diff --git a/cou/apps/factory.py b/cou/apps/factory.py index 82abf027..13b693f6 100644 --- a/cou/apps/factory.py +++ b/cou/apps/factory.py @@ -22,7 +22,7 @@ from juju.client._definitions import ApplicationStatus from cou.apps.base import OpenStackApplication -from cou.utils.juju_utils import COUMachine, COUModel +from cou.utils.juju_utils import COUModel, Machine from cou.utils.openstack import is_charm_supported logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def create( config: dict, model: COUModel, charm: str, - machines: dict[str, COUMachine], + machines: dict[str, Machine], ) -> Optional[OpenStackApplication]: """Create the OpenStackApplication or registered subclasses. @@ -49,8 +49,8 @@ def create( decorator can be instantiated and used with their customized methods. :param name: Name of the application :type name: str - :param machines: COUMachine in the model - :type machines: dict[str, COUMachine] + :param machines: Machine in the model + :type machines: dict[str, Machine] :param status: Status of the application :type status: ApplicationStatus :param config: Configuration of the application diff --git a/cou/utils/juju_utils.py b/cou/utils/juju_utils.py index a2200a5a..2c59bc7b 100644 --- a/cou/utils/juju_utils.py +++ b/cou/utils/juju_utils.py @@ -131,7 +131,7 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any: # pylint: disable=W9011 @dataclass(frozen=True) -class COUMachine: +class Machine: """Representation of a juju machine.""" machine_id: str @@ -510,7 +510,7 @@ async def _wait_for_active_idle() -> None: await _wait_for_active_idle() - async def get_machines(self) -> dict[str, COUMachine]: + async def get_machines(self) -> dict[str, Machine]: """Get all the machines in the model. :return: Dictionary of the machines found in the model. E.g: {'0': Machine0} @@ -519,7 +519,7 @@ async def get_machines(self) -> dict[str, COUMachine]: model = await self._get_model() return { - machine.id: COUMachine( + machine.id: Machine( machine_id=machine.id, apps=tuple( unit.application diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c86f271a..360734a5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -26,7 +26,7 @@ from cou.apps.base import ApplicationUnit, OpenStackApplication from cou.apps.core import Keystone from cou.apps.subordinate import OpenStackSubordinateApplication -from cou.utils.juju_utils import COUMachine +from cou.utils.juju_utils import Machine from cou.utils.openstack import OpenStackRelease STANDARD_AZS = ["zone-1", "zone-2", "zone-3"] @@ -122,7 +122,7 @@ def apps_machines(): def _generate_apps_machines(charm, machines, azs): machines_azs = zip(machines, azs) - return {charm: {machine_id: COUMachine(machine_id, (), az) for machine_id, az in machines_azs}} + return {charm: {machine_id: Machine(machine_id, (), az) for machine_id, az in machines_azs}} @pytest.fixture diff --git a/tests/unit/steps/test_steps_analyze.py b/tests/unit/steps/test_steps_analyze.py index 10f587a2..8ca823a1 100644 --- a/tests/unit/steps/test_steps_analyze.py +++ b/tests/unit/steps/test_steps_analyze.py @@ -18,7 +18,7 @@ from cou.apps.base import ApplicationUnit, OpenStackApplication from cou.steps import analyze from cou.steps.analyze import Analysis -from cou.utils.juju_utils import COUMachine +from cou.utils.juju_utils import Machine def test_analysis_dump(apps, model): @@ -195,7 +195,7 @@ def _app(name, units): def _unit(machine_id): unit = MagicMock(spec_set=ApplicationUnit).return_value - unit.machine = COUMachine(machine_id, (), "zone-1") + unit.machine = Machine(machine_id, (), "zone-1") return unit diff --git a/tests/unit/utils/test_juju_utils.py b/tests/unit/utils/test_juju_utils.py index 9a79672f..d4a56e88 100644 --- a/tests/unit/utils/test_juju_utils.py +++ b/tests/unit/utils/test_juju_utils.py @@ -174,15 +174,15 @@ async def func(self): ], ) def test_machine_not_eq(machine_id, az): - machine_0 = juju_utils.COUMachine("0", (), "zone-1") - machine_1 = juju_utils.COUMachine(machine_id, (), az) + machine_0 = juju_utils.Machine("0", (), "zone-1") + machine_1 = juju_utils.Machine(machine_id, (), az) assert machine_0 != machine_1 def test_machine_eq(): - machine_0 = juju_utils.COUMachine("0", (), "zone-1") - machine_1 = juju_utils.COUMachine("0", (), "zone-1") + machine_0 = juju_utils.Machine("0", (), "zone-1") + machine_1 = juju_utils.Machine("0", (), "zone-1") assert machine_0 == machine_1 @@ -578,7 +578,7 @@ async def test_coumodel_wait_for_active_idle_timeout(mock_get_supported_apps, mo async def test_get_machines(mocked_model): """Test COUModel getting machines from model.""" expected_machines = { - "0": juju_utils.COUMachine( + "0": juju_utils.Machine( "0", ( "app1", @@ -586,8 +586,8 @@ async def test_get_machines(mocked_model): ), "zone-1", ), - "1": juju_utils.COUMachine("1", ("app1",), "zone-2"), - "2": juju_utils.COUMachine("2", ("app1",), "zone-3"), + "1": juju_utils.Machine("1", ("app1",), "zone-2"), + "2": juju_utils.Machine("2", ("app1",), "zone-3"), } mocked_model.machines = {f"{i}": _generate_juju_machine(f"{i}") for i in range(3)} mocked_model.units = { From 2b6f9cf0b59059ba94296071ae56f325b8e5620d Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Sat, 16 Mar 2024 00:48:30 +0100 Subject: [PATCH 6/6] Update factory.py Co-authored-by: TQ X --- cou/apps/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cou/apps/factory.py b/cou/apps/factory.py index 13b693f6..167dd42a 100644 --- a/cou/apps/factory.py +++ b/cou/apps/factory.py @@ -49,7 +49,7 @@ def create( decorator can be instantiated and used with their customized methods. :param name: Name of the application :type name: str - :param machines: Machine in the model + :param machines: Machines in the model :type machines: dict[str, Machine] :param status: Status of the application :type status: ApplicationStatus