From 17b6508a5a0714585b8cffdef580e4df6ac13910 Mon Sep 17 00:00:00 2001 From: Gabriel Cocenza Date: Tue, 6 Feb 2024 19:40:58 -0300 Subject: [PATCH] Introduction of Nova Compute class. --- cou/apps/base.py | 104 +++++++++++++++++++++++++++++++------- cou/apps/machine.py | 7 ++- cou/apps/nova_compute.py | 98 +++++++++++++++++++++++++++++++++++ cou/exceptions.py | 6 ++- cou/utils/nova_compute.py | 9 ++++ 5 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 cou/apps/nova_compute.py diff --git a/cou/apps/base.py b/cou/apps/base.py index edb27181..b6904243 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -333,18 +333,29 @@ def current_os_release(self) -> OpenStackRelease: :return: OpenStackRelease object :rtype: OpenStackRelease """ - os_versions = defaultdict(list) + os_versions_units = self._get_os_from_units() + if self.upgrade_by_unit: + return self._os_release_to_upgrade_by_unit(os_versions_units) + return self._os_release_to_upgrade_all_in_one(os_versions_units) + + def _get_os_from_units(self): + os_versions_units = defaultdict(list) for unit in self.units: - os_versions[unit.os_version].append(unit.name) + os_versions_units[unit.os_version].append(unit.name) + return os_versions_units + + def _os_release_to_upgrade_by_unit(self, os_versions_units) -> OpenStackRelease: + return min(os_versions_units.keys()) - if len(os_versions.keys()) == 1: - return next(iter(os_versions)) + def _os_release_to_upgrade_all_in_one(self, os_versions_units) -> OpenStackRelease: + if len(os_versions_units.keys()) == 1: + return next(iter(os_versions_units)) # NOTE (gabrielcocenza) on applications that use single-unit or paused-single-unit # upgrade methods, more than one version can be found. mismatched_repr = [ f"'{openstack_release.codename}': {units}" - for openstack_release, units in os_versions.items() + for openstack_release, units in os_versions_units.items() ] raise MismatchedOpenStackVersions( @@ -409,6 +420,15 @@ def can_upgrade_current_channel(self) -> bool: """ return bool(self.status.can_upgrade_to) + @property + def upgrade_by_unit(self) -> bool: + """Check if application should upgrade by unit, also known as "paused-single-unit" strategy. + + :return: whether if application should upgrade by unit or not. + :rtype: bool + """ + return any(machine.is_hypervisor for machine in self.machines) + def new_origin(self, target: OpenStackRelease) -> str: """Return the new openstack-origin or source configuration. @@ -419,7 +439,7 @@ def new_origin(self, target: OpenStackRelease) -> str: """ return f"cloud:{self.series}-{target.codename}" - async def _check_upgrade(self, target: OpenStackRelease) -> None: + async def _check_upgrade(self, target: OpenStackRelease, units: list[ApplicationUnit]) -> None: """Check if an application has upgraded its workload version. :param target: OpenStack release as target to upgrade. @@ -429,6 +449,7 @@ async def _check_upgrade(self, target: OpenStackRelease) -> None: status = await self.model.get_status() app_status = status.applications.get(self.name) units_not_upgraded = [] + # change the logic to check by units passed or all application units if no unit is passed for unit in app_status.units.keys(): workload_version = app_status.units[unit].workload_version compatible_os_versions = OpenStackCodenameLookup.find_compatible_versions( @@ -456,7 +477,9 @@ def pre_upgrade_steps(self, target: OpenStackRelease) -> list[PreUpgradeStep]: self._get_refresh_charm_step(target), ] - def upgrade_steps(self, target: OpenStackRelease) -> list[UpgradeStep]: + def upgrade_steps( + self, target: OpenStackRelease, units: list[ApplicationUnit] + ) -> list[UpgradeStep]: """Upgrade steps planning. :param target: OpenStack release as target to upgrade. @@ -474,9 +497,10 @@ def upgrade_steps(self, target: OpenStackRelease) -> list[UpgradeStep]: raise HaltUpgradePlanGeneration(msg) return [ - self._get_disable_action_managed_step(), + self._get_disable_or_enable_action_managed_step(units), self._get_upgrade_charm_step(target), - self._get_workload_upgrade_step(target), + self._get_change_install_repository_step(target), + self._get_workload_upgrade_steps(units), ] def post_upgrade_steps(self, target: OpenStackRelease) -> list[PostUpgradeStep]: @@ -494,7 +518,9 @@ def post_upgrade_steps(self, target: OpenStackRelease) -> list[PostUpgradeStep]: self._get_reached_expected_target_step(target), ] - def generate_upgrade_plan(self, target: OpenStackRelease) -> ApplicationUpgradePlan: + def generate_upgrade_plan( + self, target: OpenStackRelease, machines: list[Machine] = [] + ) -> ApplicationUpgradePlan: """Generate full upgrade plan for an Application. :param target: OpenStack codename to upgrade. @@ -502,12 +528,13 @@ def generate_upgrade_plan(self, target: OpenStackRelease) -> ApplicationUpgradeP :return: Full upgrade plan if the Application is able to generate it. :rtype: ApplicationUpgradePlan """ + units = self._machines_to_units(machines) upgrade_steps = ApplicationUpgradePlan( description=f"Upgrade plan for '{self.name}' to {target}", ) all_steps = ( self.pre_upgrade_steps(target) - + self.upgrade_steps(target) + + self.upgrade_steps(target, units) + self.post_upgrade_steps(target) ) for step in all_steps: @@ -515,6 +542,11 @@ def generate_upgrade_plan(self, target: OpenStackRelease) -> ApplicationUpgradeP upgrade_steps.add_step(step) return upgrade_steps + def _machines_to_units(self, machines: list[Machine]) -> list[ApplicationUnit]: + if machines: + return [unit for unit in self.units if unit.machine in machines] + return [] + def _get_upgrade_current_release_packages_step(self) -> PreUpgradeStep: """Get step for upgrading software packages to the latest of the current release. @@ -599,6 +631,13 @@ def _get_upgrade_charm_step(self, target: OpenStackRelease) -> UpgradeStep: ) return UpgradeStep() + def _get_disable_or_enable_action_managed_step( + self, units: list[ApplicationUnit] + ) -> UpgradeStep: + if units: + return self._get_enable_action_managed_step() + return self._get_disable_action_managed_step() + def _get_disable_action_managed_step(self) -> UpgradeStep: """Get step to disable action-managed-upgrade. @@ -650,6 +689,14 @@ def _get_pause_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: ), ) + def _get_openstack_upgrade_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + return UnitUpgradeStep( + description=(f"Upgrade the unit: '{unit.name}'."), + coro=self.model.run_action( + unit_name=unit.name, action_name="openstack-upgrade", raise_on_failure=True + ), + ) + def _get_resume_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: """Get the step to resume a unit after upgrading the workload version. @@ -665,13 +712,11 @@ def _get_resume_unit_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: ), ) - def _get_workload_upgrade_step(self, target: OpenStackRelease) -> UpgradeStep: - """Get workload upgrade step by changing openstack-origin or source. + def _get_change_install_repository_step(self, target: OpenStackRelease) -> UpgradeStep: + """Change openstack-origin or source for the next OpenStack target. :param target: OpenStack release as target to upgrade. :type target: OpenStackRelease - :return: Workload upgrade step - :rtype: UpgradeStep """ if self.os_origin != self.new_origin(target) and self.origin_setting: return UpgradeStep( @@ -683,15 +728,40 @@ def _get_workload_upgrade_step(self, target: OpenStackRelease) -> UpgradeStep: self.name, {self.origin_setting: self.new_origin(target)} ), ) + logger.warning( - "Not triggering the workload upgrade of app %s: %s already set to %s", + "Not changing the install repository of app %s: %s already set to %s", self.name, self.origin_setting, self.new_origin(target), ) return UpgradeStep() - def _get_reached_expected_target_step(self, target: OpenStackRelease) -> PostUpgradeStep: + def _get_workload_upgrade_steps( + self, units: list[ApplicationUnit] + ) -> list[list[UnitUpgradeStep]]: + """Get workload upgrade step. + + If no units is passed, that means the "All-in-one" strategy was done. + :param target: OpenStack release as target to upgrade. + :type target: OpenStackRelease + :return: Workload upgrade step + :rtype: UpgradeStep + """ + units_steps = [] + if units: + for unit in units: + unit_steps = [ + self._get_pause_unit_step(unit), + self._get_openstack_upgrade_step(unit), + self._get_resume_unit_step(unit), + ] + units_steps.append(unit_steps) + return units_steps + + def _get_reached_expected_target_step( + self, target: OpenStackRelease, units: list[ApplicationUnit] + ) -> PostUpgradeStep: """Get post upgrade step to check if application workload has been upgraded. :param target: OpenStack release as target to upgrade. diff --git a/cou/apps/machine.py b/cou/apps/machine.py index 04d6e4e4..41c6421b 100644 --- a/cou/apps/machine.py +++ b/cou/apps/machine.py @@ -14,7 +14,7 @@ """Machine class.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional @@ -25,6 +25,11 @@ class Machine: machine_id: str hostname: str az: Optional[str] # simple deployments may not have azs + charms_deployed: set[str] = field(default_factory=set) + + @property + def is_hypervisor(self) -> bool: + return "nova-compute" in self.charms_deployed def __repr__(self) -> str: """Representation of the juju Machine. diff --git a/cou/apps/nova_compute.py b/cou/apps/nova_compute.py new file mode 100644 index 00000000..5a4fb467 --- /dev/null +++ b/cou/apps/nova_compute.py @@ -0,0 +1,98 @@ +# Copyright 2023 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Nova Compute application class.""" +import logging + +from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.apps.factory import AppFactory +from cou.steps import UnitUpgradeStep +from cou.utils.nova_compute import _get_instance_count_to_upgrade + +logger = logging.getLogger(__name__) + + +@AppFactory.register_application(["keystone"]) +class NovaCompute(OpenStackApplication): + """Nova Compute application. + + Nova Compute must wait for the entire model to be idle before declaring the upgrade complete. + """ + + wait_timeout = 30 * 60 # 30 min + wait_for_model = True + force_upgrade = False + + @property + def need_canary_node(self) -> bool: + os_versions_units = self._get_os_from_units() + return len(os_versions_units.keys()) == 1 + + def _get_workload_upgrade_steps( + self, units: list[ApplicationUnit] + ) -> list[list[UnitUpgradeStep]]: + units_steps = [] + + if self.need_canary_node: + units = [units[0]] + + for unit in units: + unit_steps = [ + self._get_disable_scheduler_step(unit), + self._get_empty_hypervisor_check(unit), + self._get_pause_unit_step(unit), + self._get_openstack_upgrade_step(unit), + self._get_resume_unit_step(unit), + self._get_enable_scheduler_step(unit), + ] + units_steps.append(unit_steps) + return units_steps + + def _get_empty_hypervisor_check(self, unit) -> UnitUpgradeStep: + if self.force: + return UnitUpgradeStep() + return UnitUpgradeStep( + description="Run the instance-count to upgrade", + coro=_get_instance_count_to_upgrade(unit), + ) + + def _get_enable_scheduler_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + """Get the step to enable the scheduler, so the unit can create new VMs. + + :param unit: Unit to be enabled. + :type unit: ApplicationUnit + :return: Step to enable the scheduler + :rtype: UnitUpgradeStep + """ + return UnitUpgradeStep( + description=f"Pause the unit: '{unit.name}'.", + coro=self.model.run_action( + unit_name=unit.name, action_name="enable", raise_on_failure=True + ), + ) + + def _get_disable_scheduler_step(self, unit: ApplicationUnit) -> UnitUpgradeStep: + """Get the step to disable the scheduler, so the unit cannot create new VMs. + + :param unit: Unit to be disabled. + :type unit: ApplicationUnit + :return: Step to enable the scheduler + :rtype: UnitUpgradeStep + """ + return UnitUpgradeStep( + description=f"Pause the unit: '{unit.name}'.", + coro=self.model.run_action( + unit_name=unit.name, action_name="enable", raise_on_failure=True + ), + ) diff --git a/cou/exceptions.py b/cou/exceptions.py index ba66c2e8..e7b6c745 100644 --- a/cou/exceptions.py +++ b/cou/exceptions.py @@ -56,7 +56,11 @@ class NoTargetError(COUException): class HaltUpgradePlanGeneration(COUException): - """Exception to halt the application upgrade at any moment.""" + """Exception to halt the application upgrade plan generation at any moment.""" + + +class HaltUpgradeExecution(COUException): + """Exception to halt the application upgrade at any moment""" class ApplicationError(COUException): diff --git a/cou/utils/nova_compute.py b/cou/utils/nova_compute.py index 62afb29a..93e35332 100644 --- a/cou/utils/nova_compute.py +++ b/cou/utils/nova_compute.py @@ -18,6 +18,7 @@ from cou.apps.base import ApplicationUnit from cou.apps.machine import Machine +from cou.exceptions import HaltUpgradeExecution from cou.utils.juju_utils import COUModel @@ -60,3 +61,11 @@ async def get_instance_count(unit: str, model: COUModel) -> int: f"No valid instance count value found in the result of {action_name} action " f"running on '{unit}': {action.results}" ) + + +async def _get_instance_count_to_upgrade(unit: ApplicationUnit, model: COUModel) -> None: + unit_instance_count = await get_instance_count(unit, model) + if unit_instance_count != 0: + model.run_action(unit_name=unit.name, action_name="enable", raise_on_failure=True) + # log warning message + raise HaltUpgradeExecution(f"Unit: {unit.name} has {unit_instance_count} VMs running")