diff --git a/cou/apps/auxiliary.py b/cou/apps/auxiliary.py index 8c0f9290..6527320e 100644 --- a/cou/apps/auxiliary.py +++ b/cou/apps/auxiliary.py @@ -158,7 +158,7 @@ def _get_change_require_osd_release_step(self) -> PreUpgradeStep: :return: Step to check and set correct value for require-osd-release :rtype: PreUpgradeStep """ - ceph_mon_unit, *_ = self.units + ceph_mon_unit, *_ = self.units.values() return PreUpgradeStep( description="Ensure require-osd-release option matches with ceph-osd version", coro=set_require_osd_release_option(ceph_mon_unit.name, self.model), @@ -177,7 +177,7 @@ def pre_upgrade_steps(self, target: OpenStackRelease) -> list[PreUpgradeStep]: :return: List of pre upgrade steps. :rtype: list[PreUpgradeStep] """ - for unit in self.units: + for unit in self.units.values(): validate_ovn_support(unit.workload_version) return super().pre_upgrade_steps(target) diff --git a/cou/apps/auxiliary_subordinate.py b/cou/apps/auxiliary_subordinate.py index fee3c52b..bd4ca941 100644 --- a/cou/apps/auxiliary_subordinate.py +++ b/cou/apps/auxiliary_subordinate.py @@ -50,5 +50,5 @@ def pre_upgrade_steps(self, target: OpenStackRelease) -> list[PreUpgradeStep]: :return: List of pre upgrade steps. :rtype: list[PreUpgradeStep] """ - validate_ovn_support(self.status.workload_version) + validate_ovn_support(self.workload_version) return super().pre_upgrade_steps(target) diff --git a/cou/apps/base.py b/cou/apps/base.py index 62219a34..fda916b1 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -21,7 +21,6 @@ from io import StringIO from typing import Any, Optional -from juju.client._definitions import ApplicationStatus, UnitStatus from ruamel.yaml import YAML from cou.exceptions import ( @@ -37,7 +36,7 @@ UpgradeStep, ) from cou.utils.app_utils import upgrade_packages -from cou.utils.juju_utils import COUMachine, COUModel +from cou.utils.juju_utils import COUApplication, COUUnit from cou.utils.openstack import ( DISTRO_TO_OPENSTACK_MAPPING, OpenStackCodenameLookup, @@ -50,39 +49,9 @@ @dataclass(frozen=True) -class ApplicationUnit: - """Representation of a single unit of application.""" - - name: str - os_version: OpenStackRelease - machine: COUMachine - workload_version: str = "" - - def __repr__(self) -> str: - """Representation of the application unit. - - :return: Representation of the application unit - :rtype: str - """ - return f"Unit[{self.name}]-Machine[{self.machine.machine_id}]" - - -@dataclass -class OpenStackApplication: +class OpenStackApplication(COUApplication): """Representation of a charmed OpenStack application in the deployment. - :param name: Name of the application - :type name: str - :param status: Status of the application. - :type status: ApplicationStatus - :param config: Configuration of the application. - :type config: dict - :param model: COUModel object - :type model: COUModel - :param charm: Name of the charm. - :type charm: str - :param units: Units representation of an application. - :type units: list[ApplicationUnit] :raises ApplicationError: When there are no compatible OpenStack release for the workload version. :raises MismatchedOpenStackVersions: When units part of this application are running mismatched @@ -92,15 +61,6 @@ class OpenStackApplication: :raises RunUpgradeError: When an upgrade fails. """ - # pylint: disable=too-many-instance-attributes - - name: str - status: ApplicationStatus - config: dict - model: COUModel - charm: str - machines: dict[str, COUMachine] - units: list[ApplicationUnit] = field(default_factory=lambda: []) packages_to_hold: Optional[list] = field(default=None, init=False) wait_timeout: int = field(default=DEFAULT_WAITING_TIMEOUT, init=False) wait_for_model: bool = field(default=False, init=False) # waiting only for application itself @@ -108,7 +68,6 @@ class OpenStackApplication: def __post_init__(self) -> None: """Initialize the Application dataclass.""" self._verify_channel() - self._populate_units() def __hash__(self) -> int: """Hash magic method for Application. @@ -116,7 +75,7 @@ def __hash__(self) -> int: :return: Unique hash identifier for Application object. :rtype: int """ - return hash(f"{self.name}{self.charm}") + return hash(f"{self.name}({self.charm})") def __eq__(self, other: Any) -> bool: """Equal magic method for Application. @@ -138,15 +97,15 @@ def __str__(self) -> str: self.name: { "model_name": self.model.name, "charm": self.charm, - "charm_origin": self.charm_origin, + "charm_origin": self.origin, "os_origin": self.os_origin, "channel": self.channel, "units": { unit.name: { "workload_version": unit.workload_version, - "os_version": str(unit.os_version), + "os_version": str(self._get_latest_os_version(unit)), } - for unit in self.units + for unit in self.units.values() }, } } @@ -160,59 +119,18 @@ def _verify_channel(self) -> None: :raises ApplicationError: Exception raised when channel is not a valid OpenStack channel. """ - if self.is_from_charm_store or self.is_valid_track(self.status.charm_channel): - logger.debug("%s app has proper channel %s", self.name, self.status.charm_channel) + if self.is_from_charm_store or self.is_valid_track(self.channel): + logger.debug("%s app has proper channel %s", self.name, self.channel) return raise ApplicationError( - f"Channel: {self.status.charm_channel} for charm '{self.charm}' on series " + f"Channel: {self.channel} for charm '{self.charm}' on series " f"'{self.series}' is currently not supported in this tool. Please take a look at the " "documentation: " "https://docs.openstack.org/charm-guide/latest/project/charm-delivery.html to see if " "you are using the right track." ) - def _populate_units(self) -> None: - """Populate application units.""" - if not self.is_subordinate: - for name, unit in self.status.units.items(): - compatible_os_version = self._get_latest_os_version(unit) - self.units.append( - ApplicationUnit( - name=name, - workload_version=unit.workload_version, - os_version=compatible_os_version, - machine=self.machines[unit.machine], - ) - ) - - @property - def is_subordinate(self) -> bool: - """Check if application is subordinate. - - :return: True if subordinate, False otherwise. - :rtype: bool - """ - return bool(self.status.subordinate_to) - - @property - def channel(self) -> str: - """Get charm channel of the application. - - :return: Charm channel. E.g: ussuri/stable - :rtype: str - """ - return self.status.charm_channel - - @property - def charm_origin(self) -> str: - """Get the charm origin of application. - - :return: Charm origin. E.g: cs or ch - :rtype: str - """ - return self.status.charm.split(":")[0] - @property def os_origin(self) -> str: """Get application configuration for openstack-origin or source. @@ -240,15 +158,6 @@ def origin_setting(self) -> Optional[str]: return None - @property - def is_from_charm_store(self) -> bool: - """Check if application comes from charm store. - - :return: True if comes, False otherwise. - :rtype: bool - """ - return self.charm_origin == "cs" - def is_valid_track(self, charm_channel: str) -> bool: """Check if the channel track is valid. @@ -263,11 +172,11 @@ def is_valid_track(self, charm_channel: str) -> bool: except ValueError: return self.is_from_charm_store - def _get_latest_os_version(self, unit: UnitStatus) -> OpenStackRelease: + def _get_latest_os_version(self, unit: COUUnit) -> OpenStackRelease: """Get the latest compatible OpenStack release based on the unit workload version. :param unit: Application Unit - :type unit: UnitStatus + :type unit: COUUnit :raises ApplicationError: When there are no compatible OpenStack release for the workload version. :return: The latest compatible OpenStack release. @@ -314,15 +223,6 @@ def target_channel(self, target: OpenStackRelease) -> str: """ return f"{target.codename}/stable" - @property - def series(self) -> str: - """Ubuntu series of the application. - - :return: Ubuntu series of application. E.g: focal - :rtype: str - """ - return self.status.series - @property def current_os_release(self) -> OpenStackRelease: """Current OpenStack Release of the application. @@ -333,8 +233,9 @@ def current_os_release(self) -> OpenStackRelease: :rtype: OpenStackRelease """ os_versions = defaultdict(list) - for unit in self.units: - os_versions[unit.os_version].append(unit.name) + for unit in self.units.values(): + os_version = self._get_latest_os_version(unit) + os_versions[os_version].append(unit.name) if len(os_versions.keys()) == 1: return next(iter(os_versions)) @@ -406,7 +307,7 @@ def can_upgrade_current_channel(self) -> bool: :return: True if can upgrade, False otherwise. :rtype: bool """ - return bool(self.status.can_upgrade_to) + return bool(self.can_upgrade_to) def new_origin(self, target: OpenStackRelease) -> str: """Return the new openstack-origin or source configuration. @@ -529,8 +430,8 @@ def _get_upgrade_current_release_packages_step(self) -> PreUpgradeStep: for unit in self.units: step.add_step( UnitUpgradeStep( - description=f"Upgrade software packages on unit {unit.name}", - coro=upgrade_packages(unit.name, self.model, self.packages_to_hold), + description=f"Upgrade software packages on unit {unit}", + coro=upgrade_packages(unit, self.model, self.packages_to_hold), ) ) @@ -565,7 +466,7 @@ def _get_refresh_charm_step(self, target: OpenStackRelease) -> PreUpgradeStep: channel, ) - if self.charm_origin == "cs": + if self.origin == "cs": description = f"Migration of '{self.name}' from charmstore to charmhub" switch = f"ch:{self.charm}" elif self.channel in self.possible_current_channels: @@ -643,11 +544,11 @@ def _get_enable_action_managed_step(self) -> UpgradeStep: coro=self.model.set_application_config(self.name, {"action-managed-upgrade": True}), ) - def _get_pause_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + def _get_pause_unit_step(self, unit: COUUnit) -> UnitUpgradeStep: """Get the step to pause a unit to upgrade. :param unit: Unit to be paused. - :type unit: ApplicationUnit + :type unit: COUUnit :return: Step to pause a unit. :rtype: UnitUpgradeStep """ @@ -658,11 +559,11 @@ def _get_pause_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: ), ) - def _get_resume_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + def _get_resume_unit_step(self, unit: COUUnit) -> UnitUpgradeStep: """Get the step to resume a unit after upgrading the workload version. :param unit: Unit to be resumed. - :type unit: ApplicationUnit + :type unit: COUUnit :return: Step to resume a unit. :rtype: UnitUpgradeStep """ @@ -673,11 +574,11 @@ def _get_resume_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: ), ) - def _get_openstack_upgrade_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + def _get_openstack_upgrade_step(self, unit: COUUnit) -> UnitUpgradeStep: """Get the step to upgrade a unit. :param unit: Unit to be upgraded. - :type unit: ApplicationUnit + :type unit: COUUnit :return: Step to upgrade a unit. :rtype: UnitUpgradeStep """ diff --git a/cou/apps/channel_based.py b/cou/apps/channel_based.py index 0fa0b3b2..38ef6bf4 100644 --- a/cou/apps/channel_based.py +++ b/cou/apps/channel_based.py @@ -14,11 +14,10 @@ """Channel based application class.""" import logging -from juju.client._definitions import UnitStatus - from cou.apps.base import OpenStackApplication from cou.apps.factory import AppFactory from cou.steps import PostUpgradeStep +from cou.utils.juju_utils import COUUnit from cou.utils.openstack import CHANNEL_BASED_CHARMS, OpenStackRelease logger = logging.getLogger(__name__) @@ -28,11 +27,11 @@ class OpenStackChannelBasedApplication(OpenStackApplication): """Application for charms that are channel based.""" - def _get_latest_os_version(self, unit: UnitStatus) -> OpenStackRelease: + def _get_latest_os_version(self, unit: COUUnit) -> OpenStackRelease: """Get the latest compatible OpenStack release based on the channel. - :param unit: Application Unit - :type unit: UnitStatus + :param unit: COUUnit + :type unit: COUUnit :raises ApplicationError: When there are no compatible OpenStack release for the workload version. :return: The latest compatible OpenStack release. @@ -58,7 +57,7 @@ def is_versionless(self) -> bool: :return: True if is versionless, False otherwise. :rtype: bool """ - return not all(unit.workload_version for unit in self.status.units.values()) + return not all(unit.workload_version for unit in self.units.values()) def post_upgrade_steps(self, target: OpenStackRelease) -> list[PostUpgradeStep]: """Post Upgrade steps planning. diff --git a/cou/apps/factory.py b/cou/apps/factory.py index 375fdb01..0f5ce3e0 100644 --- a/cou/apps/factory.py +++ b/cou/apps/factory.py @@ -19,10 +19,8 @@ from collections.abc import Callable from typing import Optional -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 COUApplication from cou.utils.openstack import is_charm_supported logger = logging.getLogger(__name__) @@ -34,48 +32,37 @@ class AppFactory: charms: dict[str, type[OpenStackApplication]] = {} @classmethod - def create( - cls, - name: str, - status: ApplicationStatus, - config: dict, - model: COUModel, - charm: str, - machines: dict[str, COUMachine], - ) -> Optional[OpenStackApplication]: + def create(cls, app: COUApplication) -> Optional[OpenStackApplication]: """Create the OpenStackApplication or registered subclasses. Applications Subclasses registered with the "register_application" 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, COUMachine] - :param status: Status of the application - :type status: ApplicationStatus - :param config: Configuration of the application - :type config: dict - :param model: COUModel object - :type model: COUModel - :param charm: Name of the charm - :type charm: str + :param app: COUApplication + :type app: COUApplication :return: The OpenStackApplication class or None if not supported. :rtype: Optional[OpenStackApplication] """ # pylint: disable=too-many-arguments - if is_charm_supported(charm): - app_class = cls.charms.get(charm, OpenStackApplication) + if is_charm_supported(app.charm): + app_class = cls.charms.get(app.charm, OpenStackApplication) return app_class( - name=name, - status=status, - config=config, - model=model, - charm=charm, - machines=machines, + name=app.name, + can_upgrade_to=app.can_upgrade_to, + charm=app.charm, + channel=app.channel, + config=app.config, + machines=app.machines, + model=app.model, + origin=app.origin, + series=app.series, + subordinate_to=app.subordinate_to, + units=app.units, + workload_version=app.workload_version, ) + logger.debug( "'%s' is not a supported OpenStack related application and will be ignored.", - name, + app.name, ) return None diff --git a/cou/steps/analyze.py b/cou/steps/analyze.py index 90c652b4..47fca19b 100644 --- a/cou/steps/analyze.py +++ b/cou/steps/analyze.py @@ -79,12 +79,12 @@ def is_data_plane(app: OpenStackApplication) -> bool: control_plane, data_plane = [], [] data_plane_machines = { - unit.machine for app in apps if is_data_plane(app) for unit in app.units + unit.machine for app in apps if is_data_plane(app) for unit in app.units.values() } 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): + elif any(unit.machine in data_plane_machines for unit in app.units.values()): data_plane.append(app) else: control_plane.append(app) @@ -120,23 +120,8 @@ async def _populate(cls, model: juju_utils.COUModel) -> list[OpenStackApplicatio :return: Application objects with their respective information. :rtype: List[OpenStackApplication] """ - juju_status = await model.get_status() - juju_machines = await model.get_machines() - apps = { - AppFactory.create( - name=app, - status=app_status, - config=await model.get_application_config(app), - model=model, - charm=await model.get_charm_name(app), - 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 - } + juju_applications = await model.get_applications() + apps = {AppFactory.create(app) for app in juju_applications.values()} # remove non-supported charms that return None on AppFactory.create apps.discard(None) diff --git a/cou/steps/hypervisor.py b/cou/steps/hypervisor.py index c7642936..547d50d9 100644 --- a/cou/steps/hypervisor.py +++ b/cou/steps/hypervisor.py @@ -16,13 +16,14 @@ import logging from dataclasses import dataclass -from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.apps.base import OpenStackApplication from cou.steps import ( HypervisorUpgradePlan, PostUpgradeStep, PreUpgradeStep, UpgradePlan, ) +from cou.utils.juju_utils import COUUnit from cou.utils.openstack import OpenStackRelease logger = logging.getLogger(__name__) @@ -33,7 +34,7 @@ class HypervisorMachine: """Hypervisor machine containing units for multiple applications.""" name: str - units: list[ApplicationUnit] + units: list[COUUnit] @dataclass(frozen=True) diff --git a/cou/steps/plan.py b/cou/steps/plan.py index 2f7c3b38..0b46475c 100644 --- a/cou/steps/plan.py +++ b/cou/steps/plan.py @@ -419,7 +419,7 @@ async def _get_upgradable_hypervisors_machines( nova_compute_units = [ unit for app in analysis_result.apps_data_plane - for unit in app.units + for unit in app.units.values() if app.charm == "nova-compute" ] diff --git a/cou/utils/nova_compute.py b/cou/utils/nova_compute.py index 75094b30..0b78b126 100644 --- a/cou/utils/nova_compute.py +++ b/cou/utils/nova_compute.py @@ -16,15 +16,14 @@ import asyncio -from cou.apps.base import ApplicationUnit -from cou.utils.juju_utils import COUMachine, COUModel +from cou.utils.juju_utils import COUMachine, COUModel, COUUnit -async def get_empty_hypervisors(units: list[ApplicationUnit], model: COUModel) -> list[COUMachine]: +async def get_empty_hypervisors(units: list[COUUnit], model: COUModel) -> list[COUMachine]: """Get the empty hypervisors in the model. :param units: all nova-compute units. - :type units: list[ApplicationUnit] + :type units: list[COUUnit] :param model: COUModel object :type model: COUModel :return: List with just the empty hypervisors machines. diff --git a/tests/unit/apps/test_auxiliary.py b/tests/unit/apps/test_auxiliary.py index f3c54f73..f8b35338 100644 --- a/tests/unit/apps/test_auxiliary.py +++ b/tests/unit/apps/test_auxiliary.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Auxiliary application class.""" +from unittest.mock import MagicMock, patch + import pytest from cou.apps.auxiliary import ( @@ -20,7 +22,6 @@ OvnPrincipalApplication, RabbitMQServer, ) -from cou.apps.base import ApplicationUnit from cou.exceptions import ApplicationError, HaltUpgradePlanGeneration from cou.steps import ( ApplicationUpgradePlan, @@ -30,83 +31,105 @@ UpgradeStep, ) from cou.utils import app_utils +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps +from tests.unit.utils import assert_steps -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=apps_machines["rmq"]["0/lxd/19"], - ) - ] +def test_auxiliary_app(model): + """Test auxiliary application. + The version 3.8 on rabbitmq can be from ussuri to yoga. In that case it will be + set as yoga. + """ + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=[], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) assert app.channel == "3.8/stable" assert app.is_valid_track(app.channel) is True assert app.os_origin == "distro" - assert app.units == expected_units assert app.apt_source_codename == "ussuri" assert app.channel_codename == "yoga" assert app.is_subordinate is False assert app.current_os_release == "yoga" -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=apps_machines["rmq"]["0/lxd/19"], - ) - ] - rmq_status = status["rabbitmq_server"] - rmq_status.charm = "cs:amd64/focal/rabbitmq-server-638" - rmq_status.charm_channel = "stable" +def test_auxiliary_app_cs(model): + """Test auxiliary application from charm store.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=[], + charm="rabbitmq-server", + channel="stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="cs", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) + assert app.channel == "stable" assert app.is_valid_track(app.channel) is True assert app.os_origin == "distro" - assert app.units == expected_units assert app.apt_source_codename == "ussuri" assert app.channel_codename == "ussuri" assert app.current_os_release == "yoga" -def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel( - status, config, model, apps_machines -): +def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel(model): + """Test auxiliary upgrade plan from Ussuri to Victoria with change of channel.""" target = OpenStackRelease("victoria") + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=["3.9/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -114,7 +137,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.status.units.keys(): + for unit in app.units.keys(): expected_upgrade_package_step.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit}", @@ -159,25 +182,35 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_change_channel( ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) -def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): +def test_auxiliary_upgrade_plan_ussuri_to_victoria(model): + """Test auxiliary upgrade plan from Ussuri to Victoria.""" target = OpenStackRelease("victoria") - rmq_status = status["rabbitmq_server"] - # rabbitmq already on channel 3.9 on ussuri - rmq_status.charm_channel = "3.9/stable" + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - rmq_status, - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=["3.9/stable"], + charm="rabbitmq-server", + channel="3.9/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.9", + machine=machines["0"], + ) + }, + workload_version="3.9", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -185,7 +218,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model, apps_m description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -224,25 +257,36 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria(status, config, model, apps_m ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration( - status, config, model, apps_machines -): +def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration(model): + """Test auxiliary upgrade plan from Ussuri to Victoria with migration to charmhub.""" target = OpenStackRelease("victoria") - rmq_status = status["rabbitmq_server"] - rmq_status.charm = "cs:amd64/focal/rabbitmq-server-638" - rmq_status.charm_channel = "stable" + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=["3.9/stable"], + charm="rabbitmq-server", + channel="stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="cs", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) - upgrade_plan = app.generate_upgrade_plan(target) + expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}", ) @@ -250,7 +294,7 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -294,143 +338,277 @@ def test_auxiliary_upgrade_plan_ussuri_to_victoria_ch_migration( ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) -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" - with pytest.raises(ApplicationError): + +def test_auxiliary_upgrade_plan_unknown_track(model): + """Test auxiliary upgrade plan with unknown track.""" + channel = "2.0/stable" + exp_msg = ( + f"Channel: {channel} for charm 'rabbitmq-server' on series 'focal' is currently " + "not supported in this tool. Please take a look at the documentation: " + "https://docs.openstack.org/charm-guide/latest/project/charm-delivery.html " + "to see if you are using the right track." + ) + machines = {"0": MagicMock(spec_set=COUMachine)} + with pytest.raises(ApplicationError, match=exp_msg): RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=["3.9/stable"], + charm="rabbitmq-server", + channel=channel, + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) -def test_auxiliary_app_unknown_version_raise_ApplicationError( - status, config, model, apps_machines -): - with pytest.raises(ApplicationError): +def test_auxiliary_app_unknown_version_raise_ApplicationError(model): + """Test auxiliary upgrade plan with unknown version.""" + version = "80.5" + charm = "rabbitmq-server" + exp_msg = f"'{charm}' with workload version {version} has no compatible OpenStack release." + + machines = {"0": MagicMock(spec_set=COUMachine)} + app = RabbitMQServer( + name=charm, + can_upgrade_to=["3.8/stable"], + charm=charm, + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version=version, + machine=machines["0"], + ) + }, + workload_version=version, + ) + with pytest.raises(ApplicationError, match=exp_msg): + app._get_latest_os_version(app.units[f"{charm}/0"]) + + +def test_auxiliary_raise_error_unknown_series(model): + """Test auxiliary upgrade plan with unknown series.""" + series = "foo" + channel = "3.8/stable" + exp_msg = ( + f"Channel: {channel} for charm 'rabbitmq-server' on series '{series}' is currently " + "not supported in this tool. Please take a look at the documentation: " + "https://docs.openstack.org/charm-guide/latest/project/charm-delivery.html " + "to see if you are using the right track." + ) + machines = {"0": MagicMock(spec_set=COUMachine)} + with pytest.raises(ApplicationError, match=exp_msg): RabbitMQServer( - "rabbitmq-server", - status["unknown_rabbitmq_server"], - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=["3.9/stable"], + charm="rabbitmq-server", + channel=channel, + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series=series, + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) -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): - RabbitMQServer( - "rabbitmq-server", - app_status, - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], - ) +@patch("cou.apps.core.OpenStackApplication.current_os_release") +def test_auxiliary_raise_error_os_not_on_lookup(current_os_release, model): + """Test auxiliary upgrade plan with os release not in lookup table. + Using OpenStack release version that is not on openstack_to_track_mapping.csv table. + """ + current_os_release.return_value = OpenStackRelease("diablo") -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", - new_callable=mocker.PropertyMock, - return_value=OpenStackRelease("diablo"), - ) - app_status = status["rabbitmq_server"] + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - app_status, - config["auxiliary_ussuri"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name="rabbitmq-server", + can_upgrade_to=[], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) + with pytest.raises(ApplicationError): app.possible_current_channels -def test_auxiliary_raise_halt_upgrade(status, config, model, apps_machines): +def test_auxiliary_raise_halt_upgrade(model): + """Test auxiliary upgrade plan halt the upgrade. + + The source is already configured to wallaby, so the plan halt with target victoria + """ target = OpenStackRelease("victoria") - # source is already configured to wallaby, so the plan halt with target victoria + charm = "rabbitmq-server" + exp_msg = ( + f"Application '{charm}' already configured for release equal or greater than {target}. " + "Ignoring." + ) + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - status["rabbitmq_server"], - config["auxiliary_wallaby"], - model, - "rabbitmq-server", - apps_machines["rmq"], - ) - with pytest.raises(HaltUpgradePlanGeneration): + name=charm, + can_upgrade_to=[], + charm=charm, + channel="3.8/stable", + config={"source": {"value": "cloud:focal-wallaby"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", + ) + + with pytest.raises(HaltUpgradePlanGeneration, match=exp_msg): app.generate_upgrade_plan(target) -def test_auxiliary_no_suitable_channel(status, config, model, apps_machines): - # OPENSTACK_TO_TRACK_MAPPING can't find a track for rabbitmq, focal, zed. +def test_auxiliary_no_suitable_channel(model): + """Test auxiliary upgrade plan not suitable channel. + + The OPENSTACK_TO_TRACK_MAPPING can't find a track for rabbitmq, focal, zed. + """ target = OpenStackRelease("zed") - app_status = status["rabbitmq_server"] - app_status.series = "focal" + charm = "rabbitmq-server" + exp_msg = ( + f"Cannot find a suitable '{charm}' charm channel for {target} on series 'focal'. " + "Please take a look at the documentation: " + "https://docs.openstack.org/charm-guide/latest/project/charm-delivery.html" + ) + machines = {"0": MagicMock(spec_set=COUMachine)} app = RabbitMQServer( - "rabbitmq-server", - app_status, - config["auxiliary_wallaby"], - model, - "rabbitmq-server", - apps_machines["rmq"], + name=charm, + can_upgrade_to=[], + charm=charm, + channel="3.8/stable", + config={"source": {"value": "cloud:focal-wallaby"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) - with pytest.raises(ApplicationError): + + with pytest.raises(ApplicationError, match=exp_msg): app.target_channel(target) -def test_ceph_mon_app(status, config, model, apps_machines): +def test_ceph_mon_app(model): """Test the correctness of instantiating CephMonApplication.""" + charm = "ceph-mon" + machines = {"0": MagicMock(spec_set=COUMachine)} app = CephMonApplication( - "ceph-mon", - status["ceph_mon_pacific"], - config["auxiliary_xena"], - model, - "ceph-mon", - apps_machines["ceph-mon"], + name=charm, + can_upgrade_to=[], + charm=charm, + channel="pacific/stable", + config={"source": {"value": "cloud:focal-xena"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="16.2.0", + machine=machines["0"], + ) + }, + workload_version="16.2.0", ) + assert app.channel == "pacific/stable" assert app.os_origin == "cloud:focal-xena" - assert app.units == [ - ApplicationUnit( - name="ceph-mon/0", - os_version=OpenStackRelease("xena"), - workload_version="16.2.0", - machine=apps_machines["ceph-mon"]["6"], - ) - ] + assert app._get_latest_os_version(app.units[f"{charm}/0"]) == OpenStackRelease("xena") assert app.apt_source_codename == "xena" assert app.channel_codename == "xena" assert app.is_subordinate is False -def test_ceph_mon_upgrade_plan_xena_to_yoga(status, config, model, apps_machines): +def test_ceph_mon_upgrade_plan_xena_to_yoga(model): """Test when ceph version changes between os releases.""" target = OpenStackRelease("yoga") + charm = "ceph-mon" + machines = {"0": MagicMock(spec_set=COUMachine)} app = CephMonApplication( - "ceph-mon", - status["ceph_mon_pacific"], - config["auxiliary_xena"], - model, - "ceph-mon", - apps_machines["ceph-mon"], + name=charm, + can_upgrade_to=["quincy/stable"], + charm=charm, + channel="pacific/stable", + config={"source": {"value": "cloud:focal-xena"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="16.2.0", + machine=machines["0"], + ) + }, + workload_version="16.2.0", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -438,7 +616,7 @@ def test_ceph_mon_upgrade_plan_xena_to_yoga(status, config, model, apps_machines description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -486,26 +664,36 @@ def test_ceph_mon_upgrade_plan_xena_to_yoga(status, config, model, apps_machines ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_ceph_mon_upgrade_plan_ussuri_to_victoria( - status, - config, - model, - apps_machines, -): +def test_ceph_mon_upgrade_plan_ussuri_to_victoria(model): """Test when ceph version remains the same between os releases.""" target = OpenStackRelease("victoria") + charm = "ceph-mon" + machines = {"0": MagicMock(spec_set=COUMachine)} app = CephMonApplication( - "ceph-mon", - status["ceph_mon_octopus"], - config["auxiliary_ussuri"], - model, - "ceph-mon", - apps_machines["ceph-mon"], + name=charm, + can_upgrade_to=["quincy/stable"], + charm=charm, + channel="octopus/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="15.2.0", + machine=machines["0"], + ) + }, + workload_version="15.2.0", ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" @@ -514,7 +702,7 @@ def test_ceph_mon_upgrade_plan_ussuri_to_victoria( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -557,17 +745,34 @@ def test_ceph_mon_upgrade_plan_ussuri_to_victoria( ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_ovn_principal(status, config, model, apps_machines): +def test_ovn_principal(model): + """Test the correctness of instantiating OvnPrincipalApplication.""" + charm = "ovn-central" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OvnPrincipalApplication( - "ovn-central", - status["ovn_central_22"], - config["auxiliary_ussuri"], - model, - "ovn-central", - apps_machines["ovn-central"], + name=charm, + can_upgrade_to=["22.06/stable"], + charm=charm, + channel="22.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.03", ) assert app.channel == "22.03/stable" assert app.os_origin == "distro" @@ -577,57 +782,103 @@ def test_ovn_principal(status, config, model, apps_machines): assert app.is_subordinate is False -def test_ovn_workload_ver_lower_than_22_principal(status, config, model, apps_machines): +def test_ovn_workload_ver_lower_than_22_principal(model): + """Test the OvnPrincipalApplication with lower version than 22.""" target = OpenStackRelease("victoria") - - exp_error_msg_ovn_upgrade = ( + charm = "ovn-central" + exp_msg = ( "OVN versions lower than 22.03 are not supported. It's necessary to upgrade " "OVN to 22.03 before upgrading the cloud. Follow the instructions at: " "https://docs.openstack.org/charm-guide/latest/project/procedures/" "ovn-upgrade-2203.html" ) - - app_ovn_central = OvnPrincipalApplication( - "ovn-central", - status["ovn_central_20"], - config["auxiliary_ussuri"], - model, - "ovn-central", - apps_machines["ovn-central"], + machines = {"0": MagicMock(spec_set=COUMachine)} + app = OvnPrincipalApplication( + name=charm, + can_upgrade_to=["22.03/stable"], + charm=charm, + channel="20.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="20.03.2", + machine=machines["0"], + ) + }, + workload_version="20.03.2", ) - with pytest.raises(ApplicationError, match=exp_error_msg_ovn_upgrade): - app_ovn_central.generate_upgrade_plan(target) + with pytest.raises(ApplicationError, match=exp_msg): + app.generate_upgrade_plan(target) @pytest.mark.parametrize("channel", ["55.7", "19.03"]) -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): +def test_ovn_no_compatible_os_release(channel, model): + """Test the OvnPrincipalApplication with not compatible os release.""" + charm = "ovn-central" + machines = {"0": MagicMock(spec_set=COUMachine)} + exp_msg = ( + f"Channel: {channel} for charm '{charm}' on series 'focal' is currently " + "not supported in this tool. Please take a look at the documentation: " + "https://docs.openstack.org/charm-guide/latest/project/charm-delivery.html " + "to see if you are using the right track." + ) + + with pytest.raises(ApplicationError, match=exp_msg): OvnPrincipalApplication( - "ovn-central", - ovn_central_status, - config["auxiliary_ussuri"], - model, - "ovn-central", - apps_machines["ovn-central"], + name=charm, + can_upgrade_to=["quincy/stable"], + charm=charm, + channel=channel, + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.03", ) -def test_ovn_principal_upgrade_plan(status, config, model, apps_machines): +def test_ovn_principal_upgrade_plan(model): + """Test generating plan for OvnPrincipalApplication.""" target = OpenStackRelease("victoria") + charm = "ovn-central" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OvnPrincipalApplication( - "ovn-central", - status["ovn_central_22"], - config["auxiliary_ussuri"], - model, - "ovn-central", - apps_machines["ovn-central"], + name=charm, + can_upgrade_to=["22.06/stable"], + charm=charm, + channel="22.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.03", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -636,7 +887,7 @@ def test_ovn_principal_upgrade_plan(status, config, model, apps_machines): description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -654,11 +905,11 @@ def test_ovn_principal_upgrade_plan(status, config, model, apps_machines): UpgradeStep( description=( f"Change charm config of '{app.name}' " - f"'{app.origin_setting}' to 'cloud:focal-victoria'" + f"'{app.origin_setting}' to 'cloud:focal-{target}'" ), parallel=False, coro=model.set_application_config( - app.name, {f"{app.origin_setting}": "cloud:focal-victoria"} + app.name, {f"{app.origin_setting}": f"cloud:focal-{target}"} ), ), PostUpgradeStep( @@ -674,21 +925,37 @@ def test_ovn_principal_upgrade_plan(status, config, model, apps_machines): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_mysql_innodb_cluster_upgrade(status, config, model, apps_machines): +def test_mysql_innodb_cluster_upgrade(model): + """Test generating plan for MysqlInnodbClusterApplication.""" target = OpenStackRelease("victoria") - # source is already configured to wallaby, so the plan halt with target victoria + charm = "mysql-innodb-cluster" + machines = {"0": MagicMock(spec_set=COUMachine)} app = MysqlInnodbClusterApplication( - "mysql-innodb-cluster", - status["mysql_innodb_cluster"], - config["auxiliary_ussuri"], - model, - "mysql-innodb-cluster", - apps_machines["mysql-innodb-cluster"], + name=charm, + can_upgrade_to=["9.0"], + charm=charm, + channel="8.0/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"{charm}/0": COUUnit( + name=f"{charm}/0", + workload_version="8.0", + machine=machines["0"], + ) + }, + workload_version="8.0", ) - upgrade_plan = app.generate_upgrade_plan(target) + expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -696,7 +963,7 @@ def test_mysql_innodb_cluster_upgrade(status, config, model, apps_machines): description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -714,11 +981,11 @@ def test_mysql_innodb_cluster_upgrade(status, config, model, apps_machines): UpgradeStep( description=( f"Change charm config of '{app.name}' " - f"'{app.origin_setting}' to 'cloud:focal-victoria'" + f"'{app.origin_setting}' to 'cloud:focal-{target}'" ), parallel=False, coro=model.set_application_config( - app.name, {f"{app.origin_setting}": "cloud:focal-victoria"} + app.name, {f"{app.origin_setting}": f"cloud:focal-{target}"} ), ), PostUpgradeStep( @@ -734,4 +1001,6 @@ def test_mysql_innodb_cluster_upgrade(status, config, model, apps_machines): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) diff --git a/tests/unit/apps/test_auxiliary_subordinate.py b/tests/unit/apps/test_auxiliary_subordinate.py index 19dad9d1..9ec61878 100644 --- a/tests/unit/apps/test_auxiliary_subordinate.py +++ b/tests/unit/apps/test_auxiliary_subordinate.py @@ -13,6 +13,8 @@ # limitations under the License. """Tests of the Auxiliary Subordinate application class.""" +from unittest.mock import MagicMock + import pytest from cou.apps.auxiliary_subordinate import ( @@ -21,14 +23,38 @@ ) from cou.exceptions import ApplicationError from cou.steps import ApplicationUpgradePlan, PreUpgradeStep, UpgradeStep +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps +from tests.unit.utils import assert_steps + +def test_auxiliary_subordinate(model): + """Test auxiliary subordinate application.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + app = OpenStackAuxiliarySubordinateApplication( + name="keystone-mysql-router", + can_upgrade_to=[], + charm="mysql-router", + channel="8.0/stable", + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["keystone"], + units={ + "keystone-mysql-router/0": COUUnit( + name="keystone-mysql-router/0", + workload_version="8.0", + machine=machines["0"], + ) + }, + workload_version="8.0", + ) -def test_auxiliary_subordinate(apps): - app = apps["keystone_mysql_router"] assert app.channel == "8.0/stable" - assert app.charm_origin == "ch" + assert app.origin == "ch" assert app.os_origin == "" assert app.apt_source_codename is None assert app.channel_codename == "yoga" @@ -36,11 +62,31 @@ def test_auxiliary_subordinate(apps): assert app.is_subordinate is True -def test_auxiliary_subordinate_upgrade_plan_to_victoria(apps, model): +def test_auxiliary_subordinate_upgrade_plan_to_victoria(model): + """Test auxiliary subordinate application upgrade plan to victoria.""" target = OpenStackRelease("victoria") - app = apps["keystone_mysql_router"] + machines = {"0": MagicMock(spec_set=COUMachine)} + app = OpenStackAuxiliarySubordinateApplication( + name="keystone-mysql-router", + can_upgrade_to=["8.0/stable"], + charm="mysql-router", + channel="8.0/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["keystone"], + units={ + "keystone-mysql-router/0": COUUnit( + name="keystone-mysql-router/0", + workload_version="8.0", + machine=machines["0"], + ) + }, + workload_version="8.0", + ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}", ) @@ -52,13 +98,35 @@ def test_auxiliary_subordinate_upgrade_plan_to_victoria(apps, model): ), ) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_ovn_subordinate(status, model): +def test_ovn_subordinate(model): + """Test the correctness of instantiating OvnSubordinateApplication.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OvnSubordinateApplication( - "ovn-chassis", status["ovn_chassis_focal_22"], {}, model, "ovn-chassis", {} + name="ovn-chassis", + can_upgrade_to=["22.03/stable"], + charm="ovn-chassis", + channel="22.03/stable", + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ovn-chassis/0": COUUnit( + name="ovn-chassis/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.3", ) + assert app.channel == "22.03/stable" assert app.os_origin == "" assert app.apt_source_codename is None @@ -67,42 +135,66 @@ def test_ovn_subordinate(status, model): assert app.is_subordinate is True -def test_ovn_workload_ver_lower_than_22_subordinate(status, model): +def test_ovn_workload_ver_lower_than_22_subordinate(model): + """Test the OvnSubordinateApplication with lower version than 22.""" target = OpenStackRelease("victoria") - - exp_error_msg_ovn_upgrade = ( + machines = {"0": MagicMock(spec_set=COUMachine)} + exp_msg = ( "OVN versions lower than 22.03 are not supported. It's necessary to upgrade " "OVN to 22.03 before upgrading the cloud. Follow the instructions at: " "https://docs.openstack.org/charm-guide/latest/project/procedures/" "ovn-upgrade-2203.html" ) - - app_ovn_chassis = OvnSubordinateApplication( - "ovn-chassis", - status["ovn_chassis_focal_20"], - {}, - model, - "ovn-chassis", - {}, + app = OvnSubordinateApplication( + name="ovn-chassis", + can_upgrade_to=["22.03/stable"], + charm="ovn-chassis", + channel="20.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ovn-chassis/0": COUUnit( + name="ovn-chassis/0", + workload_version="20.03", + machine=machines["0"], + ) + }, + workload_version="20.3", ) - with pytest.raises(ApplicationError, match=exp_error_msg_ovn_upgrade): - app_ovn_chassis.generate_upgrade_plan(target) + with pytest.raises(ApplicationError, match=exp_msg): + app.generate_upgrade_plan(target) -def test_ovn_subordinate_upgrade_plan(status, model): +def test_ovn_subordinate_upgrade_plan(model): + """Test generating plan for OvnSubordinateApplication.""" target = OpenStackRelease("victoria") + machines = {"0": MagicMock(spec_set=COUMachine)} app = OvnSubordinateApplication( - "ovn-chassis", - status["ovn_chassis_focal_22"], - {}, - model, - "ovn-chassis", - {}, + name="ovn-chassis", + can_upgrade_to=["22.03/stable"], + charm="ovn-chassis", + channel="22.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ovn-chassis/0": COUUnit( + name="ovn-chassis/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.3", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -116,22 +208,38 @@ def test_ovn_subordinate_upgrade_plan(status, model): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) + +def test_ovn_subordinate_upgrade_plan_cant_upgrade_charm(model): + """Test generating plan for OvnSubordinateApplication failing. -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. + The 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_focal_22"] - app_status.can_upgrade_to = "" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OvnSubordinateApplication( - "ovn-chassis", - app_status, - {}, - model, - "ovn-chassis", - {}, + name="ovn-chassis", + can_upgrade_to=[], + charm="ovn-chassis", + channel="22.03/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ovn-chassis/0": COUUnit( + name="ovn-chassis/0", + workload_version="22.03", + machine=machines["0"], + ) + }, + workload_version="22.3", ) expected_plan = ApplicationUpgradePlan( @@ -139,24 +247,36 @@ def test_ovn_subordinate_upgrade_plan_cant_upgrade_charm(status, model): ) upgrade_plan = app.generate_upgrade_plan(target) - assert upgrade_plan == expected_plan - assert str(upgrade_plan) == "" + assert_steps(upgrade_plan, expected_plan) + assert not upgrade_plan -def test_ceph_dashboard_upgrade_plan_ussuri_to_victoria(status, config, model): + +def test_ceph_dashboard_upgrade_plan_ussuri_to_victoria(model): """Test when ceph version remains the same between os releases.""" target = OpenStackRelease("victoria") + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackAuxiliarySubordinateApplication( - "ceph-dashboard", - status["ceph_dashboard_octopus"], - config["auxiliary_ussuri"], - model, - "ceph-dashboard", - {}, + name="ceph-dashboard", + can_upgrade_to=["octopus/stable"], + charm="ceph-dashboard", + channel="octopus/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ceph-dashboard/0": COUUnit( + name="ceph-dashboard/0", + workload_version="15.2.0", + machine=machines["0"], + ) + }, + workload_version="15.2.0", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -170,23 +290,36 @@ def test_ceph_dashboard_upgrade_plan_ussuri_to_victoria(status, config, model): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -def test_ceph_dashboard_upgrade_plan_xena_to_yoga(status, config, model): +def test_ceph_dashboard_upgrade_plan_xena_to_yoga(model): """Test when ceph version changes between os releases.""" target = OpenStackRelease("yoga") + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackAuxiliarySubordinateApplication( - "ceph-dashboard", - status["ceph_dashboard_pacific"], - config["auxiliary_xena"], - model, - "ceph-dashboard", - {}, + name="ceph-dashboard", + can_upgrade_to=["pacific/stable"], + charm="ceph-dashboard", + channel="pacific/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "ceph-dashboard/0": COUUnit( + name="ceph-dashboard/0", + workload_version="16.2.0", + machine=machines["0"], + ) + }, + workload_version="16.2.0", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -205,4 +338,6 @@ def test_ceph_dashboard_upgrade_plan_xena_to_yoga(status, config, model): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) diff --git a/tests/unit/apps/test_base.py b/tests/unit/apps/test_base.py index 85f834d7..7e9dd128 100644 --- a/tests/unit/apps/test_base.py +++ b/tests/unit/apps/test_base.py @@ -15,29 +15,66 @@ from unittest.mock import MagicMock, patch import pytest -from juju.client._definitions import ApplicationStatus, UnitStatus -from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.apps.base import OpenStackApplication from cou.exceptions import ApplicationError from cou.steps import UnitUpgradeStep, UpgradeStep +from cou.utils.juju_utils import COUMachine, COUUnit +from tests.unit.utils import assert_steps + + +@patch("cou.apps.base.OpenStackApplication._verify_channel", return_value=None) +def test_openstack_application_magic_functions(model): + """Test OpenStackApplication magic functions, like __hash__, __eq__.""" + app = OpenStackApplication( + name="test-app", + can_upgrade_to=[], + charm="app", + channel="stable", + config={}, + machines={}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={}, + workload_version="1", + ) + + assert hash(app) == hash("test-app(app)") + assert app == app + assert app is not None @patch("cou.apps.base.OpenStackApplication._verify_channel", return_value=None) @patch("cou.utils.openstack.OpenStackCodenameLookup.find_compatible_versions") -def test_application_get_latest_os_version_failed( - mock_find_compatible_versions, config, status, model, apps_machines -): +def test_application_get_latest_os_version_failed(mock_find_compatible_versions, model): charm = "app" app_name = "my_app" - unit = MagicMock(spec_set=UnitStatus()) - unit.workload_version = "15.0.1" + unit = COUUnit( + name=f"{app_name}/0", + workload_version="1", + machine=MagicMock(spec_set=COUMachine), + ) exp_error = ( f"'{app_name}' with workload version {unit.workload_version} has no compatible OpenStack " "release." ) mock_find_compatible_versions.return_value = [] - - app = OpenStackApplication(app_name, MagicMock(), MagicMock(), MagicMock(), charm, {}) + app = OpenStackApplication( + name=app_name, + can_upgrade_to=[], + charm=charm, + channel="stable", + config={}, + machines={}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={f"{app_name}/0": unit}, + workload_version=unit.workload_version, + ) with pytest.raises(ApplicationError, match=exp_error): app._get_latest_os_version(unit) @@ -52,70 +89,133 @@ def test_application_get_latest_os_version_failed( def test_get_enable_action_managed_step(charm_config, model): charm = "app" app_name = "my_app" - status = MagicMock(spec_set=ApplicationStatus()) - status.charm_channel = "ussuri/stable" - - expected_upgrade_step = UpgradeStep( - f"Change charm config of '{app_name}' 'action-managed-upgrade' to True.", - False, - model.set_application_config(app_name, {"action-managed-upgrade": True}), - ) - if charm_config["action-managed-upgrade"]["value"]: + channel = "ussuri/stable" + if charm_config["action-managed-upgrade"]["value"] is False: + expected_upgrade_step = UpgradeStep( + f"Change charm config of '{app_name}' 'action-managed-upgrade' to True.", + False, + model.set_application_config(app_name, {"action-managed-upgrade": True}), + ) + else: expected_upgrade_step = UpgradeStep() - app = OpenStackApplication(app_name, status, charm_config, model, charm, {}) + app = OpenStackApplication( + name=app_name, + can_upgrade_to=[], + charm=charm, + channel=channel, + config=charm_config, + machines={}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={}, + workload_version="1", + ) - assert app._get_enable_action_managed_step() == expected_upgrade_step + step = app._get_enable_action_managed_step() + assert_steps(step, expected_upgrade_step) def test_get_pause_unit_step(model): charm = "app" app_name = "my_app" - status = MagicMock(spec_set=ApplicationStatus()) - status.charm_channel = "ussuri/stable" - - unit = ApplicationUnit("my_app/0", MagicMock(), MagicMock(), MagicMock()) - + channel = "ussuri/stable" + machines = {"0": MagicMock(spec_set=COUMachine)} + unit = COUUnit( + name=f"{app_name}/0", + workload_version="1", + machine=machines["0"], + ) expected_upgrade_step = UnitUpgradeStep( description=f"Pause the unit: '{unit.name}'.", - coro=model.run_action(unit_name="my_app/0", action_name="pause", raise_on_failure=True), + coro=model.run_action( + unit_name=f"{unit.name}", action_name="pause", raise_on_failure=True + ), + ) + app = OpenStackApplication( + name=app_name, + can_upgrade_to=[], + charm=charm, + channel=channel, + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={f"{unit.name}": unit}, + workload_version="1", ) - app = OpenStackApplication(app_name, status, {}, model, charm, {}) - assert app._get_pause_unit_step(unit) == expected_upgrade_step + step = app._get_pause_unit_step(unit) + assert_steps(step, expected_upgrade_step) def test_get_resume_unit_step(model): charm = "app" app_name = "my_app" - status = MagicMock(spec_set=ApplicationStatus()) - status.charm_channel = "ussuri/stable" - - unit = ApplicationUnit("my_app/0", MagicMock(), MagicMock(), MagicMock()) - + channel = "ussuri/stable" + machines = {"0": MagicMock(spec_set=COUMachine)} + unit = COUUnit( + name=f"{app_name}/0", + workload_version="1", + machine=machines["0"], + ) expected_upgrade_step = UnitUpgradeStep( description=f"Resume the unit: '{unit.name}'.", coro=model.run_action(unit_name=unit.name, action_name="resume", raise_on_failure=True), ) + app = OpenStackApplication( + name=app_name, + can_upgrade_to=[], + charm=charm, + channel=channel, + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={f"{app_name}/0": unit}, + workload_version="1", + ) - app = OpenStackApplication(app_name, status, {}, model, charm, {}) - assert app._get_resume_unit_step(unit) == expected_upgrade_step + step = app._get_resume_unit_step(unit) + assert_steps(step, expected_upgrade_step) def test_get_openstack_upgrade_step(model): charm = "app" app_name = "my_app" - status = MagicMock(spec_set=ApplicationStatus()) - status.charm_channel = "ussuri/stable" - - unit = ApplicationUnit("my_app/0", MagicMock(), MagicMock(), MagicMock()) - + channel = "ussuri/stable" + machines = {"0": MagicMock(spec_set=COUMachine)} + unit = COUUnit( + name=f"{app_name}/0", + workload_version="1", + machine=machines["0"], + ) expected_upgrade_step = UnitUpgradeStep( description=f"Upgrade the unit: '{unit.name}'.", coro=model.run_action( unit_name=unit.name, action_name="openstack-upgrade", raise_on_failure=True ), ) + app = OpenStackApplication( + name=app_name, + can_upgrade_to=[], + charm=charm, + channel=channel, + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={f"{app_name}/0": unit}, + workload_version="1", + ) - app = OpenStackApplication(app_name, status, {}, model, charm, {}) - assert app._get_openstack_upgrade_step(unit) == expected_upgrade_step + step = app._get_openstack_upgrade_step(unit) + assert_steps(step, expected_upgrade_step) diff --git a/tests/unit/apps/test_channel_based.py b/tests/unit/apps/test_channel_based.py index 832a60eb..685fd7b8 100644 --- a/tests/unit/apps/test_channel_based.py +++ b/tests/unit/apps/test_channel_based.py @@ -10,6 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from unittest.mock import MagicMock + from cou.apps.channel_based import OpenStackChannelBasedApplication from cou.steps import ( ApplicationUpgradePlan, @@ -19,86 +21,168 @@ UpgradeStep, ) from cou.utils import app_utils +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps +from tests.unit.utils import assert_steps -def test_application_versionless(status, config, model, apps_machines): +def test_application_versionless(model): + """Test application without version.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + units = { + "glance-simplestreams-sync/0": COUUnit( + name="glance-simplestreams-sync/0", + workload_version="", + machine=machines["0"], + ) + } app = OpenStackChannelBasedApplication( - "glance-simplestreams-sync", - status["glance_simplestreams_sync_focal_ussuri"], - config["openstack_ussuri"], - model, - "glance-simplestreams-sync", - apps_machines["glance-simplestreams-sync"], + name="glance-simplestreams-sync", + can_upgrade_to=[], + charm="glance-simplestreams-sync", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units=units, + workload_version="", ) + assert app.current_os_release == "ussuri" assert app.is_versionless is True + assert app._get_latest_os_version(units["glance-simplestreams-sync/0"]) == app.channel_codename -def test_application_gnocchi_ussuri(status, config, model, apps_machines): +def test_application_gnocchi_ussuri(model): + """Test the Gnocchi OpenStackChannelBasedApplication with Ussuri.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "gnocchi", - status["gnocchi_focal_ussuri"], - config["openstack_ussuri"], - model, - "gnocchi", - apps_machines["gnocchi"], + name="gnocchi", + can_upgrade_to=[], + charm="gnocchi", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "gnocchi/0": COUUnit( + name="gnocchi/0", + workload_version="4.3.4", + machine=machines["0"], + ) + }, + workload_version="4.3.4", ) + assert app.current_os_release == "ussuri" assert app.is_versionless is False -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. +def test_application_gnocchi_xena(model): + """Test the Gnocchi OpenStackChannelBasedApplication with Xena. + + The workload version is the same for xena and yoga, but current_os_release is based on + the channel. + """ + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "gnocchi", - status["gnocchi_focal_xena"], - config["openstack_xena"], - model, - "gnocchi", - apps_machines["gnocchi"], + name="gnocchi", + can_upgrade_to=[], + charm="gnocchi", + channel="xena/stable", + config={"openstack-origin": {"value": "cloud:focal-xena"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "gnocchi/0": COUUnit( + name="gnocchi/0", + workload_version="4.4.1", + machine=machines["0"], + ) + }, + workload_version="4.4.1", ) + assert app.current_os_release == "xena" assert app.is_versionless is False -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} +def test_application_designate_bind_ussuri(model): + """Test the Designate-bind OpenStackChannelBasedApplication with Ussuri. + + The workload version is the same from ussuri to yoga, but current_os_release is based on + the channel. + """ + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "designate-bind", - status["designate_bind_focal_ussuri"], - app_config, - model, - "designate-bind", - apps_machines["designate-bind"], + name="designate-bind", + can_upgrade_to=[], + charm="designate-bind", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": False}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "designate-bind/0": COUUnit( + name="designate-bind/0", + workload_version="9.16.1", + machine=machines["0"], + ) + }, + workload_version="9.16.1", ) + assert app.current_os_release == "ussuri" assert app.is_versionless is False -def test_application_versionless_upgrade_plan_ussuri_to_victoria( - status, config, model, apps_machines -): +def test_application_versionless_upgrade_plan_ussuri_to_victoria(model): + """Test generating plan for glance-simplestreams-sync (OpenStackChannelBasedApplication).""" target = OpenStackRelease("victoria") - app_config = config["openstack_ussuri"] - # Does not have action-managed-upgrade - app_config.pop("action-managed-upgrade") + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "glance-simplestreams-sync", - status["glance_simplestreams_sync_focal_ussuri"], - app_config, - model, - "glance-simplestreams-sync", - apps_machines["glance-simplestreams-sync"], + name="glance-simplestreams-sync", + can_upgrade_to=["ussuri/stable"], + charm="glance-simplestreams-sync", + channel="ussuri/stable", + config={"openstack-origin": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "glance-simplestreams-sync/0": COUUnit( + name="glance-simplestreams-sync/0", + workload_version="", + machine=machines["0"], + ) + }, + workload_version="", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -107,7 +191,7 @@ def test_application_versionless_upgrade_plan_ussuri_to_victoria( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -142,25 +226,42 @@ def test_application_versionless_upgrade_plan_ussuri_to_victoria( add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) -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. +def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(model): + """Test generating plan for Gnocchi (OpenStackChannelBasedApplication). + + Updating Gnocchi from ussuri to victoria increases 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} + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "gnocchi", - status["gnocchi_focal_ussuri"], - app_config, - model, - "gnocchi", - apps_machines["gnocchi"], + name="gnocchi", + can_upgrade_to=["ussuri/stable"], + charm="gnocchi", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": False}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "gnocchi/0": COUUnit( + name="gnocchi/0", + workload_version="4.3.4", + machine=machines["0"], + ) + }, + workload_version="4.3.4", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -169,7 +270,7 @@ def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(status, config, mod description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -214,26 +315,39 @@ def test_application_gnocchi_upgrade_plan_ussuri_to_victoria(status, config, mod add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) -def test_application_designate_bind_upgrade_plan_ussuri_to_victoria( - status, config, model, apps_machines -): + +def test_application_designate_bind_upgrade_plan_ussuri_to_victoria(model): + """Test generating plan for Designate-bind (OpenStackChannelBasedApplication).""" target = OpenStackRelease("victoria") - app_config = config["openstack_ussuri"] - app_config["action-managed-upgrade"] = {"value": False} + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackChannelBasedApplication( - "designate-bind", - status["designate_bind_focal_ussuri"], - app_config, - model, - "designate-bind", - apps_machines["designate-bind"], + name="designate-bind", + can_upgrade_to=["ussuri/stable"], + charm="designate-bind", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": False}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "designate-bind/0": COUUnit( + name="designate-bind/0", + workload_version="9.16.1", + machine=machines["0"], + ) + }, + workload_version="9.16.1", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -242,7 +356,7 @@ def test_application_designate_bind_upgrade_plan_ussuri_to_victoria( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -287,4 +401,6 @@ def test_application_designate_bind_upgrade_plan_ussuri_to_victoria( add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + + assert_steps(upgrade_plan, expected_plan) diff --git a/tests/unit/apps/test_core.py b/tests/unit/apps/test_core.py index b16f42a1..fac6b3ac 100644 --- a/tests/unit/apps/test_core.py +++ b/tests/unit/apps/test_core.py @@ -11,11 +11,11 @@ # 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. -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest +from juju.client._definitions import ApplicationStatus, UnitStatus -from cou.apps.base import ApplicationUnit from cou.apps.core import Keystone from cou.exceptions import ( ApplicationError, @@ -30,298 +30,146 @@ UpgradeStep, ) from cou.utils import app_utils -from cou.utils.juju_utils import COUMachine +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps +from tests.unit.utils import assert_steps -def test_repr_ApplicationUnit(): - app_unit = ApplicationUnit( - "keystone/0", - OpenStackRelease("ussuri"), - COUMachine("0", "juju-cef38-0", "zone-1"), - "17.0.1", - ) - assert repr(app_unit) == "Unit[keystone/0]-Machine[0]" - - -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_focal_ussuri"] - config_keystone_1 = config["openstack_ussuri"] - 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", - 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. - assert keystone_1 == keystone_2 - # keystone_1 is different then keystone_3 even if they have same status and config. - assert keystone_1 != keystone_3 - - -def assert_application( - app, - exp_name, - exp_series, - exp_status, - exp_config, - exp_model, - exp_charm, - exp_is_from_charm_store, - exp_os_origin, - exp_units, - exp_channel, - exp_current_os_release, - exp_possible_current_channels, - exp_target_channel, - exp_new_origin, - exp_apt_source_codename, - exp_channel_codename, - exp_is_subordinate, - exp_is_valid_track, - target, -): - assert app.name == exp_name - assert app.series == exp_series - assert app.status == exp_status - assert app.config == exp_config - assert app.model == exp_model - assert app.charm == exp_charm - assert app.is_from_charm_store == exp_is_from_charm_store - assert app.os_origin == exp_os_origin - assert app.units == exp_units - assert app.channel == exp_channel - assert app.current_os_release == exp_current_os_release - assert app.possible_current_channels == exp_possible_current_channels - assert app.target_channel(target) == exp_target_channel - assert app.new_origin(target) == exp_new_origin - assert app.apt_source_codename == exp_apt_source_codename - assert app.channel_codename == exp_channel_codename - assert app.is_subordinate == exp_is_subordinate - assert app.is_valid_track(app.channel) == exp_is_valid_track - - -def test_application_ussuri(status, config, units, model, apps_machines): - target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri"] - app_config = config["openstack_ussuri"] - exp_is_from_charm_store = False - exp_os_origin = "distro" - exp_units = units["units_ussuri"] - exp_channel = app_status.charm_channel - exp_series = app_status.series - exp_current_os_release = "ussuri" - exp_possible_current_channels = ["ussuri/stable"] - exp_target_channel = f"{target}/stable" - exp_new_origin = f"cloud:{exp_series}-{target}" - exp_apt_source_codename = exp_current_os_release - exp_channel_codename = exp_current_os_release - exp_is_subordinate = False - exp_is_valid_track = True - - app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] - ) - assert app.wait_for_model is True - assert_application( - app, - "my_keystone", - exp_series, - app_status, - app_config, - model, - "keystone", - exp_is_from_charm_store, - exp_os_origin, - exp_units, - exp_channel, - exp_current_os_release, - exp_possible_current_channels, - exp_target_channel, - exp_new_origin, - exp_apt_source_codename, - exp_channel_codename, - exp_is_subordinate, - exp_is_valid_track, - target, - ) - - -def test_application_different_wl(status, config, model, apps_machines): +def test_application_different_wl(model): """Different OpenStack Version on units if workload version is different.""" exp_error_msg = ( - "Units of application my_keystone are running mismatched OpenStack versions: " + "Units of application keystone are running mismatched OpenStack versions: " r"'ussuri': \['keystone\/0', 'keystone\/1'\], 'victoria': \['keystone\/2'\]. " "This is not currently handled." ) - app_status = status["keystone_focal_ussuri"] - app_status.units["keystone/2"].workload_version = "18.1.0" - app_config = config["openstack_ussuri"] + machines = { + "0": MagicMock(spec_set=COUMachine), + "1": MagicMock(spec_set=COUMachine), + "2": MagicMock(spec_set=COUMachine), + } + units = { + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.0.1", + machine=machines["0"], + ), + "keystone/1": COUUnit( + name="keystone/1", + workload_version="17.0.1", + machine=machines["1"], + ), + "keystone/2": COUUnit( + name="keystone/2", + workload_version="18.1.0", + machine=machines["2"], + ), + } app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units=units, + workload_version="18.1.0", ) + with pytest.raises(MismatchedOpenStackVersions, match=exp_error_msg): app.current_os_release -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"] - app_status.charm = "cs:amd64/focal/keystone-638" - - app_config = config["openstack_ussuri"] - exp_os_origin = "distro" - exp_units = units["units_ussuri"] - exp_channel = app_status.charm_channel - exp_is_from_charm_store = True - exp_series = app_status.series - exp_current_os_release = "ussuri" - exp_possible_current_channels = ["ussuri/stable"] - exp_target_channel = f"{target}/stable" - exp_new_origin = f"cloud:{exp_series}-{target}" - exp_apt_source_codename = exp_current_os_release - exp_channel_codename = exp_current_os_release - exp_is_subordinate = False - exp_is_valid_track = True - +def test_application_no_origin_config(model): + """Test Keystone application without origin.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] - ) - assert_application( - app, - "my_keystone", - exp_series, - app_status, - app_config, - model, - "keystone", - exp_is_from_charm_store, - exp_os_origin, - exp_units, - exp_channel, - exp_current_os_release, - exp_possible_current_channels, - exp_target_channel, - exp_new_origin, - exp_apt_source_codename, - exp_channel_codename, - exp_is_subordinate, - exp_is_valid_track, - target, - ) - - -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_focal_wallaby"] - exp_os_origin = "cloud:focal-wallaby" - exp_channel = app_status.charm_channel - exp_series = app_status.series - exp_current_os_release = "wallaby" - exp_possible_current_channels = ["wallaby/stable"] - exp_target_channel = f"{target}/stable" - exp_new_origin = f"cloud:{exp_series}-{target}" - exp_apt_source_codename = exp_current_os_release - exp_channel_codename = exp_current_os_release - exp_is_subordinate = False - exp_is_valid_track = True - - app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] - ) - assert_application( - app, - "my_keystone", - exp_series, - app_status, - app_config, - model, - "keystone", - exp_is_from_charm_store, - exp_os_origin, - exp_units, - exp_channel, - exp_current_os_release, - exp_possible_current_channels, - exp_target_channel, - exp_new_origin, - exp_apt_source_codename, - exp_channel_codename, - exp_is_subordinate, - exp_is_valid_track, - target, + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="18.0.1", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - -def test_application_no_origin_config(status, model, apps_machines): - app = Keystone( - "my_keystone", - status["keystone_focal_ussuri"], - {}, - model, - "keystone", - apps_machines["keystone"], - ) assert app.os_origin == "" assert app.apt_source_codename is None -def test_application_empty_origin_config(status, model, apps_machines): +def test_application_empty_origin_config(model): + """Test Keystone application with empty origin.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", - status["keystone_focal_ussuri"], - {"source": {"value": ""}}, - model, - "keystone", - apps_machines["keystone"], + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": ""}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="18.0.1", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) + assert app.apt_source_codename is None -def test_application_unexpected_channel(status, config, model, apps_machines): +def test_application_unexpected_channel(model): + """Test Keystone application with unexpected channel.""" target = OpenStackRelease("xena") - app_status = status["keystone_focal_wallaby"] - # channel is set to a previous OpenStack release - app_status.charm_channel = "ussuri/stable" + exp_msg = ( + "'keystone' has unexpected channel: 'ussuri/stable' for the current workload version " + "and OpenStack release: 'wallaby'. Possible channels are: wallaby/stable" + ) + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", - app_status, - config["openstack_wallaby"], - model, - "keystone", - apps_machines["keystone"], + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": ""}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="19.0.1", + machine=machines["0"], + ) + }, + workload_version="19.1.0", ) - with pytest.raises(ApplicationError): + + with pytest.raises(ApplicationError, match=exp_msg): app.generate_upgrade_plan(target) @@ -329,76 +177,147 @@ def test_application_unexpected_channel(status, config, model, apps_machines): "source_value", ["ppa:myteam/ppa", "cloud:xenial-proposed/ocata", "http://my.archive.com/ubuntu main"], ) -def test_application_unknown_source(status, model, source_value, apps_machines): +def test_application_unknown_source(source_value, model): + """Test Keystone application with unknown source.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + exp_msg = f"'keystone' has an invalid 'source': {source_value}" app = Keystone( - "my_keystone", - status["keystone_focal_ussuri"], - {"source": {"value": source_value}}, - model, - "keystone", - apps_machines["keystone"], + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": source_value}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="19.0.1", + machine=machines["0"], + ) + }, + workload_version="19.1.0", ) - with pytest.raises(ApplicationError): + + with pytest.raises(ApplicationError, match=exp_msg): app.apt_source_codename @pytest.mark.asyncio -async def test_application_check_upgrade(status, config, model, apps_machines): +async def test_application_check_upgrade(model): + """Test Kyestone application check successful upgrade.""" target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri"] - app_config = config["openstack_ussuri"] + machines = {"0": MagicMock(spec_set=COUMachine)} + app = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.0.1", + machine=machines["0"], + ) + }, + workload_version="17.1.0", + ) # workload version changed from ussuri to victoria mock_status = AsyncMock() - mock_status.return_value.applications = {"my_keystone": status["keystone_focal_victoria"]} + mock_app_status = MagicMock(spec_set=ApplicationStatus()) + mock_unit_status = MagicMock(spec_set=UnitStatus()) + mock_unit_status.workload_version = "18.1.0" + mock_app_status.units = {"keystone/0": mock_unit_status} + mock_status.return_value.applications = {"keystone": mock_app_status} model.get_status = mock_status - 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, apps_machines): - exp_error_msg = "Cannot upgrade units 'keystone/0, keystone/1, keystone/2' to victoria." +async def test_application_check_upgrade_fail(model): + """Test Kyestone application check unsuccessful upgrade.""" target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri"] - app_config = config["openstack_ussuri"] + exp_msg = "Cannot upgrade units 'keystone/0' to victoria." + machines = {"0": MagicMock(spec_set=COUMachine)} + app = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.0.1", + machine=machines["0"], + ) + }, + workload_version="17.1.0", + ) # workload version didn't change from ussuri to victoria mock_status = AsyncMock() - mock_status.return_value.applications = {"my_keystone": app_status} + mock_app_status = MagicMock(spec_set=ApplicationStatus()) + mock_unit_status = MagicMock(spec_set=UnitStatus()) + mock_unit_status.workload_version = "17.1.0" + mock_app_status.units = {"keystone/0": mock_unit_status} + mock_status.return_value.applications = {"keystone": mock_app_status} model.get_status = mock_status - app = Keystone( - "my_keystone", - app_status, - app_config, - model, - "keystone", - machines=apps_machines["keystone"], - ) - with pytest.raises(ApplicationError, match=exp_error_msg): + + with pytest.raises(ApplicationError, match=exp_msg): await app._check_upgrade(target) -def test_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): +def test_upgrade_plan_ussuri_to_victoria(model): + """Test generate plan to upgrade Keystone from Ussuri to Victoria.""" target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri"] - app_config = config["openstack_ussuri"] + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", - app_status, - app_config, - model, - "keystone", - machines=apps_machines["keystone"], + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="17.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="17.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -406,7 +325,7 @@ def test_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -454,20 +373,38 @@ def test_upgrade_plan_ussuri_to_victoria(status, config, model, apps_machines): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) -def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model, apps_machines): +def test_upgrade_plan_ussuri_to_victoria_ch_migration(model): + """Test generate plan to upgrade Keystone from Ussuri to Victoria with charmhub migration.""" target = OpenStackRelease("victoria") - - app_status = status["keystone_focal_ussuri"] - app_status.charm = "cs:amd64/focal/keystone-638" - - app_config = config["openstack_ussuri"] + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="cs", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="17.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="17.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -475,7 +412,7 @@ def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model, app description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -523,20 +460,41 @@ def test_upgrade_plan_ussuri_to_victoria_ch_migration(status, config, model, app ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) + +def test_upgrade_plan_channel_on_next_os_release(model): + """Test generate plan to upgrade Keystone from Ussuri to Victoria with updated channel. -def test_upgrade_plan_channel_on_next_os_release(status, config, model, apps_machines): + The app channel it's already on next OpenStack release. + """ target = OpenStackRelease("victoria") - 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" + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=["victoria/stable"], + charm="keystone", + channel="victoria/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="17.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="17.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -545,7 +503,7 @@ def test_upgrade_plan_channel_on_next_os_release(status, config, model, apps_mac description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -583,21 +541,41 @@ def test_upgrade_plan_channel_on_next_os_release(status, config, model, apps_mac ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) + +def test_upgrade_plan_origin_already_on_next_openstack_release(model): + """Test generate plan to upgrade Keystone from Ussuri to Victoria with origin changed. -def test_upgrade_plan_origin_already_on_next_openstack_release( - status, config, model, apps_machines -): + The app config option openstack-origin it's already on next OpenStack release. + """ target = OpenStackRelease("victoria") - 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" + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "cloud:focal-victoria"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="17.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="17.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -605,7 +583,7 @@ def test_upgrade_plan_origin_already_on_next_openstack_release( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -643,36 +621,76 @@ def test_upgrade_plan_origin_already_on_next_openstack_release( ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) -def test_upgrade_plan_application_already_upgraded(status, config, model, apps_machines): +def test_upgrade_plan_application_already_upgraded(model): + """Test generate plan to upgrade Keystone from Victoria to Victoria.""" exp_error_msg = ( - "Application 'my_keystone' already configured for release equal or greater " + "Application 'keystone' already configured for release equal or greater " "than victoria. Ignoring." ) target = OpenStackRelease("victoria") - app_status = status["keystone_focal_wallaby"] - app_config = config["openstack_wallaby"] + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=[], + charm="keystone", + channel="wallaby/stable", + config={ + "openstack-origin": {"value": "cloud:focal-wallaby"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="19.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="19.1.0", ) + # 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, apps_machines -): +def test_upgrade_plan_application_already_disable_action_managed(model): + """Test generate plan to upgrade Keystone with managed upgrade disabled.""" target = OpenStackRelease("victoria") - app_status = status["keystone_focal_ussuri"] - app_config = config["openstack_ussuri"] - app_config["action-managed-upgrade"]["value"] = False + machines = {"0": MagicMock(spec_set=COUMachine)} app = Keystone( - "my_keystone", app_status, app_config, model, "keystone", apps_machines["keystone"] + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": False}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", + workload_version="17.0.1", + machine=machines["0"], + ) + for unit in range(3) + }, + workload_version="17.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -680,7 +698,7 @@ def test_upgrade_plan_application_already_disable_action_managed( description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -723,4 +741,5 @@ def test_upgrade_plan_application_already_disable_action_managed( ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) diff --git a/tests/unit/apps/test_factory.py b/tests/unit/apps/test_factory.py index 1f97f614..7a6702d2 100644 --- a/tests/unit/apps/test_factory.py +++ b/tests/unit/apps/test_factory.py @@ -14,19 +14,15 @@ from unittest.mock import MagicMock, patch from cou.apps import factory +from cou.utils.juju_utils import COUApplication @patch.object(factory, "is_charm_supported", return_value=False) def test_app_factory_not_supported_openstack_charm(mock_is_charm_supported): - charm = "my-app" - my_app = factory.AppFactory.create( - name=charm, - status=MagicMock(), - config=MagicMock(), - model=MagicMock(), - charm=charm, - machines=MagicMock(), - ) + app = MagicMock(spec_set=COUApplication)() + app.charm = charm = "my_app" + my_app = factory.AppFactory.create(app) + assert my_app is None mock_is_charm_supported.assert_called_once_with(charm) @@ -34,6 +30,8 @@ def test_app_factory_not_supported_openstack_charm(mock_is_charm_supported): @patch.object(factory, "is_charm_supported", return_value=True) def test_app_factory_register(mock_is_charm_supported): charm = "foo" + app = MagicMock(spec_set=COUApplication)() + app.charm = charm @factory.AppFactory.register_application([charm]) class Foo: @@ -41,7 +39,9 @@ def __init__(self, *_, **__): pass assert charm in factory.AppFactory.charms - foo = factory.AppFactory.create("my-foo", MagicMock(), {}, MagicMock(), charm, MagicMock()) + foo = factory.AppFactory.create(app) + 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 5f4f73d1..039bd79c 100644 --- a/tests/unit/apps/test_subordinate.py +++ b/tests/unit/apps/test_subordinate.py @@ -13,45 +13,72 @@ # limitations under the License. """Subordinate application class.""" import logging +from unittest.mock import MagicMock import pytest from cou.apps.subordinate import OpenStackSubordinateApplication from cou.exceptions import ApplicationError from cou.steps import ApplicationUpgradePlan, PreUpgradeStep, UpgradeStep +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps +from tests.unit.utils import assert_steps logger = logging.getLogger(__name__) -def test_post_init(status, model): - app_status = status["keystone_ldap_focal_ussuri"] +def test_current_os_release(model): + """Test current_os_release for OpenStackSubordinateApplication.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=["ussuri/stable"], + charm="keystone-ldap", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - assert app.channel == "ussuri/stable" - assert app.charm_origin == "ch" - assert app.os_origin == "" - assert app.is_subordinate is True - -def test_current_os_release(status, model): - app_status = status["keystone_ldap_focal_ussuri"] - app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} - ) assert app.current_os_release == OpenStackRelease("ussuri") -def test_generate_upgrade_plan(status, model): +def test_generate_upgrade_plan(model): + """Test generate upgrade plan for OpenStackSubordinateApplication.""" target = OpenStackRelease("victoria") - app_status = status["keystone_ldap_focal_ussuri"] + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=["ussuri/stable"], + charm="keystone-ldap", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -69,7 +96,8 @@ def test_generate_upgrade_plan(status, model): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) @pytest.mark.parametrize( @@ -83,11 +111,28 @@ def test_generate_upgrade_plan(status, model): "wallaby/edge", ], ) -def test_channel_valid(status, model, channel): - app_status = status["keystone_ldap_focal_ussuri"] - app_status.charm_channel = channel +def test_channel_valid(model, channel): + """Test successful validation of channel upgrade plan for OpenStackSubordinateApplication.""" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=[channel], + charm="keystone-ldap", + channel=channel, + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) assert app.channel == channel @@ -102,12 +147,30 @@ def test_channel_valid(status, model, channel): "something/stable", ], ) -def test_channel_setter_invalid(status, model, channel): - app_status = status["keystone_ldap_focal_ussuri"] - app_status.charm_channel = channel +def test_channel_setter_invalid(model, channel): + """Test unsuccessful validation of channel upgrade plan for OpenStackSubordinateApplication.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + with pytest.raises(ApplicationError): OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=[channel], + charm="keystone-ldap", + channel=channel, + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) @@ -119,17 +182,30 @@ def test_channel_setter_invalid(status, model, channel): "candidate", ], ) -def test_generate_plan_ch_migration(status, model, channel): +def test_generate_plan_ch_migration(model, channel): + """Test generate upgrade plan for OpenStackSubordinateApplication with charmhub migration.""" target = OpenStackRelease("wallaby") - app_status = status["keystone_ldap_focal_ussuri"] - app_status.charm = "cs:amd64/focal/keystone-ldap-437" - app_status.charm_channel = f"ussuri/{channel}" + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=["wallaby/stable"], + charm="keystone-ldap", + channel=f"ussuri/{channel}", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="cs", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - - upgrade_plan = app.generate_upgrade_plan(target) - expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -147,7 +223,8 @@ def test_generate_plan_ch_migration(status, model, channel): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) @pytest.mark.parametrize( @@ -159,15 +236,30 @@ def test_generate_plan_ch_migration(status, model, channel): (["xena", "yoga"]), ], ) -def test_generate_plan_from_to(status, model, from_os, to_os): - app_status = status["keystone_ldap_focal_ussuri"] - app_status.charm_channel = f"{from_os}/stable" +def test_generate_plan_from_to(model, from_os, to_os): + """Test generate upgrade plan for OpenStackSubordinateApplication from to version.""" + target = OpenStackRelease(to_os) + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=[f"{to_os}/stable"], + charm="keystone-ldap", + channel=f"{from_os}/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - - upgrade_plan = app.generate_upgrade_plan(OpenStackRelease(to_os)) - expected_plan = ApplicationUpgradePlan(description=f"Upgrade plan for '{app.name}' to {to_os}") upgrade_steps = [ PreUpgradeStep( @@ -183,7 +275,8 @@ def test_generate_plan_from_to(status, model, from_os, to_os): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) @pytest.mark.parametrize( @@ -196,14 +289,30 @@ def test_generate_plan_from_to(status, model, from_os, to_os): "yoga", ], ) -def test_generate_plan_in_same_version(status, model, from_to): - app_status = status["keystone_ldap_focal_ussuri"] - app_status.charm_channel = f"{from_to}/stable" +def test_generate_plan_in_same_version(model, from_to): + """Test generate upgrade plan for OpenStackSubordinateApplication in same version.""" + target = OpenStackRelease(from_to) + machines = {"0": MagicMock(spec_set=COUMachine)} app = OpenStackSubordinateApplication( - "my_keystone_ldap", app_status, {}, model, "keystone-ldap", {} + name="keystone-ldap", + can_upgrade_to=[f"{from_to}/stable"], + charm="keystone-ldap", + channel=f"{from_to}/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="18.1.0", + machine=machines["0"], + ) + }, + workload_version="18.1.0", ) - - upgrade_plan = app.generate_upgrade_plan(OpenStackRelease(from_to)) expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {from_to}" ) @@ -216,4 +325,5 @@ def test_generate_plan_in_same_version(status, model, from_to): ] add_steps(expected_plan, upgrade_steps) - assert upgrade_plan == expected_plan + upgrade_plan = app.generate_upgrade_plan(target) + assert_steps(upgrade_plan, expected_plan) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 9cd1f9c5..5362a70d 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -12,550 +12,16 @@ # See the License for the specific language governing permissions and # 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 import pytest -from juju.client._definitions import ApplicationStatus, UnitStatus from juju.client.client import FullStatus -from cou.apps.auxiliary import OpenStackAuxiliaryApplication -from cou.apps.auxiliary_subordinate import OpenStackAuxiliarySubordinateApplication -from cou.apps.base import ApplicationUnit, OpenStackApplication -from cou.apps.core import Keystone -from cou.apps.subordinate import OpenStackSubordinateApplication from cou.commands import CLIargs -from cou.steps.analyze import Analysis -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"] -KEYSTONE_WORKLOADS = { - "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()) - unit.workload_version = workload_version - unit.machine = machine - return unit - - -def _generate_units(units_machines_workloads): - unit = MagicMock(spec_set=UnitStatus()) - - ordered_units = OrderedDict() - for unit_machine_workload in units_machines_workloads: - unit, machine, workload = unit_machine_workload - ordered_units[unit] = _generate_unit(workload, machine) - - return ordered_units - - -@pytest.fixture -def apps_machines(): - return { - **_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 - ), - **_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): - hostnames = [f"{HOSTNAME_PREFIX}-{machine}" for machine in machines] - machines_hostnames_azs = zip(machines, hostnames, azs) - return { - charm: { - machine_id: COUMachine(machine_id=machine_id, hostname=hostname, az=az) - for machine_id, hostname, az in machines_hostnames_azs - } - } - - -@pytest.fixture -def status(): - 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(), - } - - -def generate_keystone_status(): - mock_keystone_focal_ussuri = _generate_status( - "focal", - "ussuri/stable", - "ch: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_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/focal/cinder-633", - [], - CINDER_UNITS, - CINDER_MACHINES, - CINDER_WORKLOADS["ussuri"], - ) - return {"cinder_focal_ussuri": mock_cinder_focal_ussuri} - - -def generate_nova_status(): - mock_nova_focal_ussuri = _generate_status( - "focal", - "ussuri/stable", - "ch:amd64/focal/nova-compute-638", - [], - NOVA_UNITS, - NOVA_MACHINES, - NOVA_WORKLOADS["ussuri"], - ) - return {"nova_focal_ussuri": mock_nova_focal_ussuri} - - -def generate_rmq_status(): - mock_rmq = _generate_status( - "focal", - "3.8/stable", - "ch:amd64/focal/rabbitmq-server-638", - [], - 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} - - -def generate_ceph_mon_status(): - mock_ceph_mon_octopus = _generate_status( - "focal", - "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", - [], - CEPH_MON_UNITS, - CEPH_MON_MACHINES, - CEPH_WORKLOADS["pacific"], - ) - return {"ceph_mon_octopus": mock_ceph_mon_octopus, "ceph_mon_pacific": mock_ceph_mon_pacific} - - -def generate_ceph_osd_status(): - mock_ceph_osd_octopus = _generate_status( - "focal", - "octopus/stable", - "ch:amd64/focal/ceph-osd-177", - [], - 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 { - "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_UNITS, - MY_APP_MACHINES, - "12.5", - ) - return {"my_app": mock_my_app} - - -def _generate_status( - 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_longest( - units, machines, [workload_version], fillvalue=workload_version - ) - app_mock.units = _generate_units(units_machines_workloads) - return app_mock - - -@pytest.fixture -def full_status(status, model): - mock_full_status = MagicMock() - mock_full_status.model.name = model.name - mock_full_status.applications = OrderedDict( - [ - ("keystone", status["keystone_focal_ussuri"]), - ("cinder", status["cinder_focal_ussuri"]), - ("rabbitmq-server", status["rabbitmq_server"]), - ("my_app", status["my_app"]), - ("nova-compute", status["nova_focal_ussuri"]), - ("ceph-osd", status["ceph_osd_octopus"]), - ] - ) - return mock_full_status - - -@pytest.fixture -def units(apps_machines): - units_ussuri = [] - units_wallaby = [] - units_ussuri.append( - ApplicationUnit( - name="keystone/0", - os_version=OpenStackRelease("ussuri"), - workload_version="17.0.1", - machine=apps_machines["keystone"]["0/lxd/12"], - ) - ) - units_ussuri.append( - ApplicationUnit( - name="keystone/1", - os_version=OpenStackRelease("ussuri"), - workload_version="17.0.1", - machine=apps_machines["keystone"]["1/lxd/12"], - ) - ) - units_ussuri.append( - ApplicationUnit( - name="keystone/2", - os_version=OpenStackRelease("ussuri"), - workload_version="17.0.1", - machine=apps_machines["keystone"]["2/lxd/13"], - ) - ) - units_wallaby.append( - ApplicationUnit( - name="keystone/0", - os_version=OpenStackRelease("wallaby"), - workload_version="19.1.0", - machine=apps_machines["keystone"]["0/lxd/12"], - ) - ) - units_wallaby.append( - ApplicationUnit( - name="keystone/1", - os_version=OpenStackRelease("wallaby"), - workload_version="19.1.0", - machine=apps_machines["keystone"]["1/lxd/12"], - ) - ) - units_wallaby.append( - ApplicationUnit( - name="keystone/2", - os_version=OpenStackRelease("wallaby"), - workload_version="19.1.0", - machine=apps_machines["keystone"]["2/lxd/13"], - ) - ) - return {"units_ussuri": units_ussuri, "units_wallaby": units_wallaby} - - -@pytest.fixture -def config(): - return { - "openstack_ussuri": { - "openstack-origin": {"value": "distro"}, - "action-managed-upgrade": {"value": True}, - }, - "openstack_wallaby": {"openstack-origin": {"value": "cloud:focal-wallaby"}}, - "openstack_xena": {"openstack-origin": {"value": "cloud:focal-xena"}}, - "auxiliary_ussuri": {"source": {"value": "distro"}}, - "auxiliary_wallaby": {"source": {"value": "cloud:focal-wallaby"}}, - "auxiliary_xena": {"source": {"value": "cloud:focal-xena"}}, - } - - -async def get_status(): +def get_status(): """Help function to load Juju status from json file.""" current_path = Path(__file__).parent.resolve() with open(current_path / "jujustatus.json", "r") as file: @@ -570,11 +36,8 @@ async def get_charm_name(value: str): @pytest.fixture -def model(config, apps_machines): +def model(): """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 @@ -587,98 +50,11 @@ def model(config, apps_machines): model.get_charm_name = AsyncMock(side_effect=get_charm_name) model.scp_from_unit = AsyncMock() model.set_application_config = AsyncMock() - model.get_application_config = mock_get_app_config = AsyncMock() - mock_get_app_config.side_effect = config.get - model.get_machines = machines + model.get_application_config = AsyncMock() return model -@pytest.fixture -def analysis_result(model, apps): - """Generate a simple analysis result to be used on unit-tests.""" - return Analysis( - model=model, - apps_control_plane=[apps["keystone_focal_ussuri"]], - apps_data_plane=[apps["nova_focal_ussuri"]], - ) - - -@pytest.fixture -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_focal_ussuri_status = status["keystone_ldap_focal_ussuri"] - - keystone_ussuri = Keystone( - "keystone", - keystone_focal_ussuri_status, - config["openstack_ussuri"], - model, - "keystone", - apps_machines["keystone"], - ) - keystone_wallaby = Keystone( - "keystone", - keystone_focal_wallaby_status, - config["openstack_wallaby"], - model, - "keystone", - apps_machines["keystone"], - ) - cinder_ussuri = OpenStackApplication( - "cinder", - cinder_focal_ussuri_status, - config["openstack_ussuri"], - model, - "cinder", - apps_machines["cinder"], - ) - 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", - apps_machines["rmq"], - ) - keystone_ldap = OpenStackSubordinateApplication( - "keystone-ldap", keystone_ldap_focal_ussuri_status, {}, model, "keystone-ldap", {} - ) - keystone_mysql_router = OpenStackAuxiliarySubordinateApplication( - "keystone-mysql-router", status["mysql_router"], {}, model, "mysql-router", {} - ) - nova_focal_ussuri = OpenStackApplication( - "nova-compute", - status["nova_focal_ussuri"], - config["openstack_ussuri"], - model, - "nova-compute", - apps_machines["nova-compute"], - ) - - return { - "keystone_focal_ussuri": keystone_ussuri, - "keystone_focal_wallaby": keystone_wallaby, - "cinder_focal_ussuri": cinder_ussuri, - "rmq": rmq, - "rmq_wallaby": rmq_wallaby, - "keystone_ldap_focal_ussuri": keystone_ldap, - "nova_focal_ussuri": nova_focal_ussuri, - "keystone_mysql_router": keystone_mysql_router, - } - - @pytest.fixture(scope="session", autouse=True) def cou_data(tmp_path_factory): cou_test = tmp_path_factory.mktemp("cou_test") @@ -695,11 +71,3 @@ def cli_args() -> MagicMock: """ # spec_set needs an instantiated class to be strict with the fields. return MagicMock(spec_set=CLIargs(command="plan"))() - - -def generate_mock_machine(machine_id, hostname, az): - mock_machine = MagicMock(spec_set=COUMachine(machine_id, hostname, az)) - mock_machine.machine_id = machine_id - mock_machine.hostname = hostname - mock_machine.az = az - return mock_machine diff --git a/tests/unit/steps/test_steps_analyze.py b/tests/unit/steps/test_steps_analyze.py index f40a9a95..340365a3 100644 --- a/tests/unit/steps/test_steps_analyze.py +++ b/tests/unit/steps/test_steps_analyze.py @@ -15,13 +15,16 @@ import pytest -from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.apps.auxiliary import RabbitMQServer +from cou.apps.base import OpenStackApplication +from cou.apps.core import Keystone from cou.steps import analyze from cou.steps.analyze import Analysis -from cou.utils.juju_utils import COUMachine +from cou.utils.juju_utils import COUApplication, COUMachine, COUUnit +from cou.utils.openstack import OpenStackRelease -def test_analysis_dump(apps, model): +def test_analysis_dump(model): """Test analysis dump.""" expected_result = ( "Control Plane:\n" @@ -73,32 +76,91 @@ def test_analysis_dump(apps, model): "\nCurrent minimum OS release in the cloud: ussuri\n" "\nCurrent minimum Ubuntu series in the cloud: focal\n" ) + machines = {f"{i}": MagicMock(spec_set=COUMachine) for i in range(3)} + keystone = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"keystone/{unit}": COUUnit( + name=f"keystone/{unit}", workload_version="17.0.1", machine=machines[f"{unit}"] + ) + for unit in range(3) + }, + workload_version="17.0.1", + ) + rabbitmq_server = RabbitMQServer( + name="rabbitmq-server", + can_upgrade_to=["3.8/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines={"0": machines["0"]}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", + ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"cinder/{unit}": COUUnit( + name=f"cinder/{unit}", + workload_version="16.4.2", + machine=machines[f"{unit}"], + ) + for unit in range(3) + }, + workload_version="16.4.2", + ) result = analyze.Analysis( model=model, - apps_control_plane=[ - apps["keystone_focal_ussuri"], - apps["cinder_focal_ussuri"], - apps["rmq"], - ], + apps_control_plane=[keystone, cinder, rabbitmq_server], apps_data_plane=[], ) assert str(result) == expected_result @pytest.mark.asyncio -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"]) +@patch("cou.apps.factory.AppFactory.create") +async def test_populate_model(mock_create, model): + """Test Analysis population of model.""" - machines = {} - for sub_dict in apps_machines.values(): - machines.update(sub_dict) + def mock_app(name: str) -> MagicMock: + app = MagicMock(spec_set=COUApplication)() + app.name = name + app.charm = name + return app - model.get_machines = AsyncMock(return_value=machines) + juju_apps = ["keystone", "cinder", "rabbitmq-server", "my-app", "ceph-osd", "nova-compute"] + model.get_applications.return_value = {app: mock_app(app) for app in juju_apps} + # simulate app factory returning None for custom app + mock_create.side_effect = lambda app: None if app.name == "my-app" else app - # 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 apps = await Analysis._populate(model) assert len(apps) == 5 # apps are on the UPGRADE_ORDER sequence @@ -113,40 +175,154 @@ async def test_populate_model(full_status, config, model, apps_machines): @pytest.mark.asyncio @patch.object(analyze.Analysis, "_populate", new_callable=AsyncMock) -async def test_analysis_create(mock_populate, apps, model): - """Test analysis object.""" - 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=[] +@patch.object( + analyze.Analysis, + "_split_apps", +) +async def test_analysis_create(mock_split_apps, mock_populate, model): + """Test analysis object creation.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + keystone = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.1.0", + machine=machines["0"], + ) + }, + workload_version="17.1.0", + ) + rabbitmq_server = RabbitMQServer( + name="rabbitmq-server", + can_upgrade_to=["3.8/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "cinder/0": COUUnit( + name="cinder/0", + workload_version="16.4.2", + machine=machines["0"], + ) + }, + workload_version="16.4.2", + ) + exp_apps = [keystone, rabbitmq_server, cinder] mock_populate.return_value = exp_apps + mock_split_apps.return_value = exp_apps, [] result = await Analysis.create(model=model) - assert result == expected_result + assert result.model == model + assert result.apps_control_plane == exp_apps + assert result.apps_data_plane == [] + assert result.min_os_version_control_plane == OpenStackRelease("ussuri") + assert result.min_os_version_data_plane is None + assert result.current_cloud_os_release == "ussuri" + assert result.current_cloud_series == "focal" @pytest.mark.asyncio -async def test_analysis_detect_current_cloud_os_release_different_releases(apps, model): - result = analyze.Analysis( +async def test_analysis_detect_current_cloud_os_release_different_releases(model): + machines = {"0": MagicMock(spec_set=COUMachine)} + keystone = Keystone( + name="keystone", + can_upgrade_to=["wallaby/stable"], + charm="keystone", + channel="wallaby/stable", + config={"source": {"value": "distro"}}, + machines=machines, model=model, - apps_control_plane=[ - apps["rmq"], - apps["keystone_focal_wallaby"], - apps["cinder_focal_ussuri"], - ], - apps_data_plane=[], + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="19.1.0", + machine=machines["0"], + ) + }, + workload_version="19.1.0", + ) + rabbitmq_server = RabbitMQServer( + name="rabbitmq-server", + can_upgrade_to=["3.8/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", + ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "cinder/0": COUUnit( + name="cinder/0", + workload_version="16.4.2", + machine=machines["0"], + ) + }, + workload_version="16.4.2", ) - - # current_cloud_os_release takes the minimum OpenStack version - assert result.current_cloud_os_release == "ussuri" - - -@pytest.mark.asyncio -async def test_analysis_detect_current_cloud_os_release_same_release(apps, model): result = analyze.Analysis( model=model, - apps_control_plane=[apps["cinder_focal_ussuri"], apps["keystone_focal_ussuri"]], + apps_control_plane=[rabbitmq_server, keystone, cinder], apps_data_plane=[], ) @@ -155,34 +331,76 @@ async def test_analysis_detect_current_cloud_os_release_same_release(apps, model @pytest.mark.asyncio -async def test_analysis_detect_current_cloud_series_same_series(apps, model): - result = analyze.Analysis( +async def test_analysis_detect_current_cloud_series_different_series(model): + """Check current_cloud_series getting lowest series in apps.""" + machines = {"0": MagicMock(spec_set=COUMachine)} + keystone = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, model=model, - apps_control_plane=[ - apps["rmq"], - apps["keystone_focal_wallaby"], - apps["cinder_focal_ussuri"], - ], - apps_data_plane=[], + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.1.0", + machine=machines["0"], + ) + }, + workload_version="17.1.0", + ) + rabbitmq_server = RabbitMQServer( + name="rabbitmq-server", + can_upgrade_to=["3.8/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["0"], + ) + }, + workload_version="3.8", + ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines=machines, + model=model, + origin="ch", + series="bionic", # change cinder to Bionic series + subordinate_to=[], + units={ + "cinder/0": COUUnit( + name="cinder/0", + workload_version="16.4.2", + machine=machines["0"], + ) + }, + workload_version="16.4.2", ) - - # current_cloud_series takes the minimum Ubuntu series - assert result.current_cloud_series == "focal" - - -@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_focal_ussuri"], keystone_bionic_ussuri], + apps_control_plane=[rabbitmq_server, keystone, cinder], apps_data_plane=[], ) - # current_cloud_series takes the minimum Ubuntu series + assert result.current_cloud_os_release == "ussuri" assert result.current_cloud_series == "bionic" @@ -194,7 +412,7 @@ def _app(name, units): def _unit(machine_id): - unit = MagicMock(spec_set=ApplicationUnit).return_value + unit = MagicMock(spec_set=COUUnit).return_value unit.machine = COUMachine(machine_id, "juju-efc45", "zone-1") return unit @@ -203,22 +421,22 @@ def _unit(machine_id): "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", {"0": _unit("0"), "1": _unit("1"), "2": _unit("2")})], + [_app("ceph-osd", {"3": _unit("3"), "4": _unit("4"), "5": _unit("5")})], ), ( [], [ - _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", {"0": _unit("0"), "1": _unit("1"), "2": _unit("2")}), + _app("keystone", {"0": _unit("0"), "1": _unit("1"), "2": _unit("2")}), + _app("ceph-osd", {"3": _unit("3"), "4": _unit("4"), "5": _unit("5")}), ], ), ( - [_app("keystone", [_unit("6"), _unit("7"), _unit("8")])], + [_app("keystone", {"6": _unit("6"), "7": _unit("7"), "8": _unit("8")})], [ - _app("nova-compute", [_unit("0"), _unit("1"), _unit("2")]), - _app("ceph-osd", [_unit("3"), _unit("4"), _unit("5")]), + _app("nova-compute", {"0": _unit("0"), "1": _unit("1"), "2": _unit("2")}), + _app("ceph-osd", {"3": _unit("3"), "4": _unit("4"), "5": _unit("5")}), ], ), ], @@ -231,24 +449,106 @@ def test_split_apps(exp_control_plane, exp_data_plane): @pytest.mark.asyncio -async def test_analysis_machines(apps, model, apps_machines): - result = analyze.Analysis( +async def test_analysis_machines(model): + """Test splitting of dataplane and control plane machines.""" + machines = { + "0": MagicMock(spec_set=COUMachine), + "1": MagicMock(spec_set=COUMachine), + "2": MagicMock(spec_set=COUMachine), + "3": MagicMock(spec_set=COUMachine), + "4": MagicMock(spec_set=COUMachine), + "5": MagicMock(spec_set=COUMachine), + } + keystone = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines={"3": machines["3"]}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.1.0", + machine=machines["3"], + ) + }, + workload_version="17.1.0", + ) + rabbitmq_server = RabbitMQServer( + name="rabbitmq-server", + can_upgrade_to=["3.8/stable"], + charm="rabbitmq-server", + channel="3.8/stable", + config={"source": {"value": "distro"}}, + machines={"4": machines["4"]}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "rabbitmq-server/0": COUUnit( + name="rabbitmq-server/0", + workload_version="3.8", + machine=machines["4"], + ) + }, + workload_version="3.8", + ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines={"5": machines["5"]}, model=model, - apps_control_plane=[ - apps["rmq"], - apps["keystone_focal_wallaby"], - apps["cinder_focal_ussuri"], - ], - apps_data_plane=[apps["nova_focal_ussuri"]], + origin="ch", + series="focal", + subordinate_to=[], + units={ + "cinder/0": COUUnit( + name="cinder/0", + workload_version="16.4.2", + machine=machines["5"], + ) + }, + workload_version="16.4.2", + ) + nova_compute = OpenStackApplication( + name="nova-compute", + can_upgrade_to=["ussuri/stable"], + charm="nova-compute", + channel="ussuri/stable", + config={"source": {"value": "distro"}}, + machines={f"{i}": machines[f"{i}"] for i in range(3)}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + f"nova-compute/{unit}": COUUnit( + name=f"nova-compute/{unit}", + workload_version="21.0.0", + machine=machines[f"{unit}"], + ) + for unit in range(3) + }, + workload_version="21.0.0", ) - expected_control_plane_machines = { - **apps_machines["rmq"], - **apps_machines["keystone"], - **apps_machines["cinder"], - } + result = analyze.Analysis( + model=model, + apps_control_plane=[rabbitmq_server, keystone, cinder], + apps_data_plane=[nova_compute], + ) - expected_data_plane_machines = {**apps_machines["nova-compute"]} + expected_control_plane_machines = {f"{i}": machines[f"{i}"] for i in range(3, 6)} # 3, 4, 5 + expected_data_plane_machines = {f"{i}": machines[f"{i}"] for i in range(3)} # 0, 1, 2 assert result.control_plane_machines == expected_control_plane_machines assert result.data_plane_machines == expected_data_plane_machines diff --git a/tests/unit/steps/test_steps_plan.py b/tests/unit/steps/test_steps_plan.py index 86610c0c..fe2631c1 100644 --- a/tests/unit/steps/test_steps_plan.py +++ b/tests/unit/steps/test_steps_plan.py @@ -16,6 +16,8 @@ import pytest from cou.apps.base import OpenStackApplication +from cou.apps.core import Keystone +from cou.apps.subordinate import OpenStackSubordinateApplication from cou.exceptions import ( DataPlaneCannotUpgrade, DataPlaneMachineFilterError, @@ -36,12 +38,14 @@ from cou.steps.analyze import Analysis from cou.steps.backup import backup from cou.utils import app_utils +from cou.utils.juju_utils import COUMachine, COUUnit from cou.utils.openstack import OpenStackRelease from tests.unit.apps.utils import add_steps -from tests.unit.conftest import KEYSTONE_MACHINES, NOVA_MACHINES, generate_mock_machine +from tests.unit.utils import assert_steps def generate_expected_upgrade_plan_principal(app, target, model): + """Generate expected upgrade plan for principal charms.""" expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target.codename}" ) @@ -63,7 +67,7 @@ def generate_expected_upgrade_plan_principal(app, target, model): description=f"Upgrade software packages of '{app.name}' from the current APT repositories", parallel=True, ) - for unit in app.units: + for unit in app.units.values(): upgrade_packages.add_step( UnitUpgradeStep( description=f"Upgrade software packages on unit {unit.name}", @@ -113,6 +117,7 @@ def generate_expected_upgrade_plan_principal(app, target, model): def generate_expected_upgrade_plan_subordinate(app, target, model): + """Generate expected upgrade plan for subordiante charms.""" expected_plan = ApplicationUpgradePlan( description=f"Upgrade plan for '{app.name}' to {target}" ) @@ -136,20 +141,85 @@ def generate_expected_upgrade_plan_subordinate(app, target, model): @pytest.mark.asyncio -async def test_generate_plan(apps, model, cli_args): +async def test_generate_plan(model, cli_args): + """Test generation of upgrade plan.""" cli_args.is_data_plane_command = False target = OpenStackRelease("victoria") - app_keystone = apps["keystone_focal_ussuri"] - app_cinder = apps["cinder_focal_ussuri"] - app_keystone_ldap = apps["keystone_ldap_focal_ussuri"] + # keystone = Keystone() + machines = {"0": MagicMock(spec_set=COUMachine)} + keystone = Keystone( + name="keystone", + can_upgrade_to=["ussuri/stable"], + charm="keystone", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines={}, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "keystone/0": COUUnit( + name="keystone/0", + workload_version="17.0.1", + machine=machines["0"], + ) + }, + workload_version="17.0.1", + ) + keystone_ldap = OpenStackSubordinateApplication( + name="keystone-ldap", + can_upgrade_to=["ussuri/stable"], + charm="keystone-ldap", + channel="ussuri/stable", + config={}, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=["nova-compute"], + units={ + "keystone-ldap/0": COUUnit( + name="keystone-ldap/0", + workload_version="17.0.1", + machine=machines["0"], + ) + }, + workload_version="17.0.1", + ) + cinder = OpenStackApplication( + name="cinder", + can_upgrade_to=["ussuri/stable"], + charm="cinder", + channel="ussuri/stable", + config={ + "openstack-origin": {"value": "distro"}, + "action-managed-upgrade": {"value": True}, + }, + machines=machines, + model=model, + origin="ch", + series="focal", + subordinate_to=[], + units={ + "cinder/0": COUUnit( + name="cinder/0", + workload_version="16.4.2", + machine=machines["0"], + ) + }, + workload_version="16.4.2", + ) + analysis_result = Analysis( model=model, - apps_control_plane=[app_keystone, app_cinder, app_keystone_ldap], + apps_control_plane=[keystone, cinder, keystone_ldap], apps_data_plane=[], ) - upgrade_plan = await cou_plan.generate_plan(analysis_result, cli_args) - expected_plan = UpgradePlan("Upgrade cloud from 'ussuri' to 'victoria'") expected_plan.add_step( PreUpgradeStep( @@ -169,20 +239,20 @@ async def test_generate_plan(apps, model, cli_args): ) control_plane_principals = UpgradePlan("Control Plane principal(s) upgrade plan") - keystone_plan = generate_expected_upgrade_plan_principal(app_keystone, target, model) - cinder_plan = generate_expected_upgrade_plan_principal(app_cinder, target, model) + keystone_plan = generate_expected_upgrade_plan_principal(keystone, target, model) + cinder_plan = generate_expected_upgrade_plan_principal(cinder, target, model) control_plane_principals.add_step(keystone_plan) control_plane_principals.add_step(cinder_plan) control_plane_subordinates = UpgradePlan("Control Plane subordinate(s) upgrade plan") - keystone_ldap_plan = generate_expected_upgrade_plan_subordinate( - app_keystone_ldap, target, model - ) + keystone_ldap_plan = generate_expected_upgrade_plan_subordinate(keystone_ldap, target, model) control_plane_subordinates.add_step(keystone_ldap_plan) expected_plan.add_step(control_plane_principals) expected_plan.add_step(control_plane_subordinates) - assert upgrade_plan == expected_plan + + upgrade_plan = await cou_plan.generate_plan(analysis_result, cli_args) + assert_steps(upgrade_plan, expected_plan) @pytest.mark.parametrize( @@ -423,21 +493,40 @@ async def test_create_upgrade_plan_failed(): @patch("builtins.print") -def test_plan_print_warn_manually_upgrade(mock_print, model, apps): +def test_plan_print_warn_manually_upgrade(mock_print, model): + nova_compute = MagicMock(spec_set=OpenStackApplication)() + nova_compute.name = "nova-compute" + nova_compute.current_os_release = OpenStackRelease("victoria") + nova_compute.series = "focal" + keystone = MagicMock(spec_set=OpenStackApplication)() + keystone.name = "keystone" + keystone.current_os_release = OpenStackRelease("wallaby") + keystone.series = "focal" + result = Analysis( model=model, - apps_control_plane=[apps["keystone_focal_wallaby"]], - apps_data_plane=[apps["nova_focal_ussuri"]], + apps_control_plane=[keystone], + apps_data_plane=[nova_compute], ) cou_plan.manually_upgrade_data_plane(result) mock_print.assert_called_with( - "WARNING: Please upgrade manually the data plane apps: nova-compute" + f"WARNING: Please upgrade manually the data plane apps: {nova_compute.name}" ) @patch("builtins.print") -def test_analysis_not_print_warn_manually_upgrade(mock_print, analysis_result): - cou_plan.manually_upgrade_data_plane(analysis_result) +def test_analysis_not_print_warn_manually_upgrade(mock_print, model): + keystone = MagicMock(spec_set=OpenStackApplication)() + keystone.name = "keystone" + keystone.current_os_release = OpenStackRelease("wallaby") + keystone.series = "focal" + + result = Analysis( + model=model, + apps_control_plane=[keystone], + apps_data_plane=[], + ) + cou_plan.manually_upgrade_data_plane(result) mock_print.assert_not_called() @@ -448,14 +537,13 @@ def test_verify_data_plane_cli_no_input( mock_verify_machines, mock_verify_hostnames, mock_verify_azs, - analysis_result, cli_args, ): cli_args.machines = None cli_args.hostnames = None cli_args.availability_zones = None - assert cou_plan.verify_data_plane_cli_input(cli_args, analysis_result) is None + assert cou_plan.verify_data_plane_cli_input(cli_args, MagicMock(spec_set=Analysis)()) is None mock_verify_machines.assert_not_called() mock_verify_hostnames.assert_not_called() @@ -465,10 +553,10 @@ def test_verify_data_plane_cli_no_input( @pytest.mark.parametrize( "cli_machines", [ - {NOVA_MACHINES[0]}, - {NOVA_MACHINES[1]}, - {NOVA_MACHINES[2]}, - {NOVA_MACHINES[0], NOVA_MACHINES[1], NOVA_MACHINES[2]}, + {"0"}, + {"1"}, + {"2"}, + {"0", "1", "2"}, ], ) @patch("cou.steps.plan.verify_data_plane_cli_azs") @@ -477,12 +565,15 @@ def test_verify_data_plane_cli_input_machines( mock_verify_hostnames, mock_verify_azs, cli_machines, - analysis_result, cli_args, ): cli_args.machines = cli_machines cli_args.hostnames = None cli_args.availability_zones = None + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.data_plane_machines = analysis_result.machines = { + f"{i}": MagicMock(spec_set=COUMachine)() for i in range(3) + } assert cou_plan.verify_data_plane_cli_input(cli_args, analysis_result) is None @@ -490,23 +581,16 @@ def test_verify_data_plane_cli_input_machines( mock_verify_azs.assert_not_called() -@pytest.mark.parametrize( - "nova_unit", - [0, 1, 2], -) @patch("cou.steps.plan.verify_data_plane_cli_azs") @patch("cou.steps.plan.verify_data_plane_cli_machines") -def test_verify_data_plane_cli_input_hostnames( - mock_verify_machines, - mock_verify_azs, - nova_unit, - analysis_result, - apps, - cli_args, -): - nova_machine = list(apps["nova_focal_ussuri"].machines.values())[nova_unit] +def test_verify_data_plane_cli_input_hostnames(mock_verify_machines, mock_verify_azs, cli_args): + hostname = "test-host-machine" + machine = MagicMock(spec_set=COUMachine)() + machine.hostname = hostname + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.data_plane_machines = analysis_result.machines = {"0": machine} cli_args.machines = None - cli_args.hostnames = {nova_machine.hostname} + cli_args.hostnames = {hostname} cli_args.availability_zones = None assert cou_plan.verify_data_plane_cli_input(cli_args, analysis_result) is None @@ -515,24 +599,17 @@ def test_verify_data_plane_cli_input_hostnames( mock_verify_azs.assert_not_called() -@pytest.mark.parametrize( - "nova_unit", - [0, 1, 2], -) @patch("cou.steps.plan.verify_data_plane_cli_hostnames") @patch("cou.steps.plan.verify_data_plane_cli_machines") -def test_verify_data_plane_cli_input_azs( - mock_verify_machines, - mock_verify_hostnames, - nova_unit, - analysis_result, - apps, - cli_args, -): - nova_machine = list(apps["nova_focal_ussuri"].machines.values())[nova_unit] +def test_verify_data_plane_cli_input_azs(mock_verify_machines, mock_verify_hostnames, cli_args): + az = "test-az-0" + machine = MagicMock(spec_set=COUMachine)() + machine.az = az + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.data_plane_machines = analysis_result.machines = {"0": machine} cli_args.machines = None cli_args.hostnames = None - cli_args.availability_zones = {nova_machine.az} + cli_args.availability_zones = {az} assert cou_plan.verify_data_plane_cli_input(cli_args, analysis_result) is None @@ -541,40 +618,64 @@ def test_verify_data_plane_cli_input_azs( @pytest.mark.parametrize( - "machine, exp_error_msg", + "cli_machines, exp_error_msg", [ - ({KEYSTONE_MACHINES[0]}, r"Machine.*are not considered as data-plane."), + ({"1"}, r"Machine.*are not considered as data-plane."), ({"5/lxd/18"}, r"Machine.*don't exist."), ], ) -def test_verify_data_plane_cli_machines_raise(analysis_result, machine, exp_error_msg): +def test_verify_data_plane_cli_machines_raise(cli_machines, exp_error_msg): + machine0 = MagicMock(spec_set=COUMachine)() + machine1 = MagicMock(spec_set=COUMachine)() + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.machines = {"0": machine0, "1": machine1} + analysis_result.data_plane_machines = {"0": machine0} + analysis_result.control_plane_machines = {"1": machine1} + with pytest.raises(DataPlaneMachineFilterError, match=exp_error_msg): - cou_plan.verify_data_plane_cli_machines(machine, analysis_result) + cou_plan.verify_data_plane_cli_machines(cli_machines, analysis_result) @pytest.mark.parametrize( - "app, exp_error_msg", + "cli_hostnames, exp_error_msg", [ - ("keystone", r"Hostname.*are not considered as data-plane."), - ("cinder", r"Hostname.*don't exist."), # cinder is not on the Analysis + ({"juju-c307f8-1"}, r"Hostname.*are not considered as data-plane."), + ({"juju-not-existing"}, r"Hostname.*don't exist."), ], ) -def test_verify_data_plane_cli_hostname_raise(apps, analysis_result, app, exp_error_msg): +def test_verify_data_plane_cli_hostname_raise(cli_hostnames, exp_error_msg): + machine0 = MagicMock(spec_set=COUMachine)() + machine0.hostname = "juju-c307f8-0" + machine1 = MagicMock(spec_set=COUMachine)() + machine1.hostname = "juju-c307f8-1" + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.machines = {"0": machine0, "1": machine1} + analysis_result.data_plane_machines = {"0": machine0} + analysis_result.control_plane_machines = {"1": machine1} + with pytest.raises(DataPlaneMachineFilterError, match=exp_error_msg): - machine = list(apps[f"{app}_focal_ussuri"].machines.values())[0] - cou_plan.verify_data_plane_cli_hostnames({machine.hostname}, analysis_result) + cou_plan.verify_data_plane_cli_hostnames(cli_hostnames, analysis_result) @pytest.mark.parametrize( - "azs, exp_error_msg", + "cli_azs, exp_error_msg", [ - ({"zone-foo"}, r"Availability Zone.*don't exist."), - ({"zone-1", "zone-foo"}, r"Availability Zone.*don't exist."), + ({"zone-1"}, r"Availability Zone.* are not considered as data-plane."), + ({"zone-test", "zone-foo"}, r"Availability Zone.*don't exist."), ], ) -def test_verify_data_plane_cli_azs_raise_dont_exist(analysis_result, azs, exp_error_msg): +def test_verify_data_plane_cli_azs_raise_dont_exist(cli_azs, exp_error_msg): + machine0 = MagicMock(spec_set=COUMachine)() + machine0.az = "zone-0" + machine1 = MagicMock(spec_set=COUMachine)() + machine1.az = "zone-1" + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.machines = {"0": machine0, "1": machine1} + analysis_result.data_plane_machines = {"0": machine0} + analysis_result.control_plane_machines = {"1": machine1} + with pytest.raises(DataPlaneMachineFilterError, match=exp_error_msg): - cou_plan.verify_data_plane_cli_azs(azs, analysis_result) + cou_plan.verify_data_plane_cli_azs(cli_azs, analysis_result) def test_verify_data_plane_cli_azs_raise_cannot_find(): @@ -643,13 +744,11 @@ async def test_filter_hypervisors_machines( ): empty_hypervisors_machines = { - generate_mock_machine( - str(machine_id), f"juju-c307f8-{machine_id}", f"zone-{machine_id + 1}" - ) + COUMachine(str(machine_id), f"juju-c307f8-{machine_id}", f"zone-{machine_id + 1}") for machine_id in range(2) } # assuming that machine-2 has some VMs running - non_empty_hypervisor_machine = generate_mock_machine("2", "juju-c307f8-2", "zone-3") + non_empty_hypervisor_machine = COUMachine("2", "juju-c307f8-2", "zone-3") upgradable_hypervisors = empty_hypervisors_machines if force: @@ -691,22 +790,36 @@ async def test_filter_hypervisors_machines( @pytest.mark.asyncio @patch("cou.steps.plan.get_empty_hypervisors") async def test_get_upgradable_hypervisors_machines( - mock_empty_hypervisors, - cli_force, - empty_hypervisors, - expected_result, - analysis_result, + mock_empty_hypervisors, cli_force, empty_hypervisors, expected_result ): - mock_empty_hypervisors.return_value = { - generate_mock_machine( - str(machine_id), f"juju-c307f8-{machine_id}", f"zone-{machine_id + 1}" + machines = {f"{i}": COUMachine(f"{i}", f"juju-c307f8-{i}", f"zone-{i + 1}") for i in range(3)} + nova_compute = MagicMock(spec_set=OpenStackApplication)() + nova_compute.charm = "nova-compute" + nova_compute.units = { + f"nova-compute/{i}": COUUnit( + name=f"nova-compute/{i}", + workload_version="21.0.0", + machine=machines[f"{i}"], ) - for machine_id in empty_hypervisors + for i in range(3) + } + analysis_result = MagicMock(spec_set=Analysis)() + analysis_result.data_plane_machines = analysis_result.machines = machines + analysis_result.apps_data_plane = [nova_compute] + mock_empty_hypervisors.return_value = { + machines[f"{machine_id}"] for machine_id in empty_hypervisors } hypervisors_possible_to_upgrade = await cou_plan._get_upgradable_hypervisors_machines( cli_force, analysis_result ) + if not cli_force: + mock_empty_hypervisors.assert_called_once_with( + [unit for unit in nova_compute.units.values()], analysis_result.model + ) + else: + mock_empty_hypervisors.assert_not_called() + assert { hypervisor.machine_id for hypervisor in hypervisors_possible_to_upgrade } == expected_result diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 6e8a3751..c57d1950 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -18,6 +18,8 @@ import unittest from unittest import mock +from cou.steps import BaseStep + @contextlib.contextmanager def patch_open(): @@ -89,3 +91,9 @@ def patch(self, item, return_value=None, name=None, new=None, **kwargs): started.return_value = return_value self._patches_start[name] = started setattr(self, name, started) + + +def assert_steps(step_1: BaseStep, step_2: BaseStep) -> None: + """Compare two steps and raise exception if they are different.""" + msg = f"\n{step_1}!=\n{step_2}" + assert step_1 == step_2, msg diff --git a/tests/unit/utils/test_nova_compute.py b/tests/unit/utils/test_nova_compute.py index 923e15ab..0c22680d 100644 --- a/tests/unit/utils/test_nova_compute.py +++ b/tests/unit/utils/test_nova_compute.py @@ -17,9 +17,8 @@ import pytest from juju.action import Action -from cou.apps.base import ApplicationUnit from cou.utils import nova_compute -from tests.unit.conftest import generate_mock_machine +from cou.utils.juju_utils import COUMachine, COUUnit @pytest.mark.asyncio @@ -76,11 +75,9 @@ async def test_get_empty_hypervisors( def _mock_nova_unit(nova_unit): - mock_nova_unit = MagicMock(spec_set=ApplicationUnit(MagicMock(), MagicMock(), MagicMock())) + mock_nova_unit = MagicMock(spec_set=COUUnit(MagicMock(), MagicMock(), MagicMock())) mock_nova_unit.name = f"nova-compute/{nova_unit}" - nova_machine = generate_mock_machine( - str(nova_unit), f"juju-c307f8-{nova_unit}", f"zone-{nova_unit + 1}" - ) + nova_machine = COUMachine(str(nova_unit), f"juju-c307f8-{nova_unit}", f"zone-{nova_unit + 1}") mock_nova_unit.machine = nova_machine return mock_nova_unit