Skip to content

Commit

Permalink
Introduction of Nova Compute class.
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielcocenza committed Feb 6, 2024
1 parent 45576dd commit 17b6508
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 19 deletions.
104 changes: 87 additions & 17 deletions cou/apps/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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]:
Expand All @@ -494,27 +518,35 @@ 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.
:type target: OpenStackRelease
: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:
if step:
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion cou/apps/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

"""Machine class."""

from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Optional


Expand All @@ -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.
Expand Down
98 changes: 98 additions & 0 deletions cou/apps/nova_compute.py
Original file line number Diff line number Diff line change
@@ -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
),
)
6 changes: 5 additions & 1 deletion cou/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 9 additions & 0 deletions cou/utils/nova_compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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")

0 comments on commit 17b6508

Please sign in to comment.