Skip to content

Commit

Permalink
Check possible charm downgrade when upgrading the channel (canonical#392
Browse files Browse the repository at this point in the history
)

- changed from `channel_codename` to `current_channel_os_release` to
represent the OpenStack release that the charm channel points to
- added `from` to` to be more explicit about the charm channel upgrade
- changed abbreviation of OpenStack from `os` to `o7k`
 
Closes: canonical#376
  • Loading branch information
gabrielcocenza authored May 7, 2024
1 parent 1573158 commit 1265215
Show file tree
Hide file tree
Showing 23 changed files with 731 additions and 646 deletions.
45 changes: 17 additions & 28 deletions cou/apps/auxiliary.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,9 @@ def is_valid_track(self, charm_channel: str) -> bool:
:return: True if valid, False otherwise.
:rtype: bool
"""
if self.is_from_charm_store:
logger.debug("'%s' has been installed from the charm store", self.name)
return True

current_track = self._get_track_from_channel(charm_channel)
possible_tracks = OPENSTACK_TO_TRACK_MAPPING.get(
(self.charm, self.series, self.current_os_release.codename), []
(self.charm, self.series, self.o7k_release.codename), []
)
return (
self.charm,
Expand All @@ -81,7 +77,7 @@ def expected_current_channel(self, target: OpenStackRelease) -> str:
]
else:
*_, track = OPENSTACK_TO_TRACK_MAPPING[
(self.charm, self.series, self.current_os_release.codename)
(self.charm, self.series, self.o7k_release.codename)
]

return f"{track}/stable"
Expand All @@ -107,37 +103,30 @@ def target_channel(self, target: OpenStackRelease) -> str:
)
)

@property
def channel_codename(self) -> OpenStackRelease:
"""Identify the OpenStack release set in the charm channel.
def _get_o7k_release_from_channel(self, channel: str) -> OpenStackRelease:
"""Get the OpenStack release from a channel.
Auxiliary charms can have multiple compatible OpenStack releases. In
that case, return the latest compatible OpenStack version.
Auxiliary charms can have multiple compatible OpenStack releases. In that case, return the
latest compatible OpenStack version.
:return: OpenStackRelease object
:param channel: channel to get the release
:type channel: str
:return: OpenStack release that the channel points to
:rtype: OpenStackRelease
:raises ApplicationError: When cannot identify suitable OpenStack release codename
based on the track of the charm channel.
"""
if self.need_crossgrade:
logger.debug(
"Cannot determine the OpenStack release of '%s' via its channel. Assuming Ussuri",
self.name,
)
return OpenStackRelease("ussuri")

track: str = self._get_track_from_channel(self.channel)
compatible_os_releases = TRACK_TO_OPENSTACK_MAPPING[(self.charm, self.series, track)]
track: str = self._get_track_from_channel(channel)
compatible_o7k_releases = TRACK_TO_OPENSTACK_MAPPING[(self.charm, self.series, track)]

if not compatible_os_releases:
if not compatible_o7k_releases:
raise ApplicationError(
f"Channel: {self.channel} for charm '{self.charm}' on series '{self.series}' is "
f"not supported by COU. 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."
)

return max(compatible_os_releases)
return max(compatible_o7k_releases)

def generate_upgrade_plan(
self,
Expand Down Expand Up @@ -175,9 +164,9 @@ def _need_current_channel_refresh(self, target: OpenStackRelease) -> bool:
:rtype: bool
"""
track: str = self._get_track_from_channel(self.channel)
compatible_os_releases = TRACK_TO_OPENSTACK_MAPPING[(self.charm, self.series, track)]
compatible_o7k_releases = TRACK_TO_OPENSTACK_MAPPING[(self.charm, self.series, track)]
return bool(self.can_upgrade_to) and any(
os_release <= target for os_release in compatible_os_releases
o7k_release <= target for o7k_release in compatible_o7k_releases
)


Expand Down Expand Up @@ -357,11 +346,11 @@ async def _verify_nova_compute(self, target: OpenStackRelease) -> None:
continue

for unit in app.units.values():
compatible_os_versions = OpenStackCodenameLookup.find_compatible_versions(
compatible_o7k_versions = OpenStackCodenameLookup.find_compatible_versions(
app.charm, unit.workload_version
)

if target not in compatible_os_versions:
if target not in compatible_o7k_versions:
units_not_upgraded.append(unit.name)

if units_not_upgraded:
Expand Down
127 changes: 79 additions & 48 deletions cou/apps/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def __str__(self) -> str:
"name": unit.name,
"machine": unit.machine.machine_id,
"workload_version": unit.workload_version,
"os_version": str(self.get_latest_os_version(unit)),
"o7k_version": str(self.get_latest_o7k_version(unit)),
}
for unit in self.units.values()
},
Expand Down Expand Up @@ -152,19 +152,19 @@ def apt_source_codename(self) -> OpenStackRelease:
:rtype: OpenStackRelease
"""
# means that the charm doesn't have origin setting config or is using empty string.
if not self.os_origin:
return self.current_os_release
if not self.o7k_origin:
return self.o7k_release

if self.os_origin.startswith("cloud"):
if self.o7k_origin.startswith("cloud"):
return self._extract_from_uca_source()

if self.os_origin == "distro":
if self.o7k_origin == "distro":
# find the OpenStack release based on ubuntu series
return OpenStackRelease(DISTRO_TO_OPENSTACK_MAPPING[self.series])

# probably because user set a ppa or a url
raise ApplicationError(
f"'{self.name}' has an invalid '{self.origin_setting}': {self.os_origin}"
f"'{self.name}' has an invalid '{self.origin_setting}': {self.o7k_origin}"
)

def _extract_from_uca_source(self) -> OpenStackRelease:
Expand All @@ -176,15 +176,15 @@ def _extract_from_uca_source(self) -> OpenStackRelease:
"""
# Ex: "cloud:focal-victoria" will result in "victoria"
try:
_, os_origin_parsed = self.os_origin.rsplit("-", maxsplit=1)
return OpenStackRelease(os_origin_parsed)
_, o7k_origin_parsed = self.o7k_origin.rsplit("-", maxsplit=1)
return OpenStackRelease(o7k_origin_parsed)
except ValueError as exc:
raise ApplicationError(
f"'{self.name}' has an invalid '{self.origin_setting}': {self.os_origin}"
f"'{self.name}' has an invalid '{self.origin_setting}': {self.o7k_origin}"
) from exc

@property
def channel_codename(self) -> OpenStackRelease:
def channel_o7k_release(self) -> OpenStackRelease:
"""Identify the OpenStack release set in the charm channel.
:return: OpenStackRelease object
Expand All @@ -197,21 +197,29 @@ def channel_codename(self) -> OpenStackRelease:
self.name,
)
return OpenStackRelease("ussuri")
return self._get_o7k_release_from_channel(self.channel)

# get the OpenStack release from the channel track of the application.
return OpenStackRelease(self._get_track_from_channel(self.channel))
def _get_o7k_release_from_channel(self, channel: str) -> OpenStackRelease:
"""Get the OpenStack release from a channel.
:param channel: channel to get the release
:type channel: str
:return: OpenStack release that the channel points to
:rtype: OpenStackRelease
"""
return OpenStackRelease(self._get_track_from_channel(channel))

@property
def current_os_release(self) -> OpenStackRelease:
def o7k_release(self) -> OpenStackRelease:
"""Current OpenStack Release of the application.
:return: OpenStackRelease object
:rtype: OpenStackRelease
"""
return min(self.os_release_units.keys())
return min(self.o7k_release_units.keys())

@property
def os_origin(self) -> str:
def o7k_origin(self) -> str:
"""Get application configuration for openstack-origin or source.
:return: Configuration parameter of the charm to set OpenStack origin.
Expand Down Expand Up @@ -251,21 +259,21 @@ def expected_current_channel(self, target: OpenStackRelease) -> str:
"""
if self.need_crossgrade and self.based_on_channel:
return f"{target.previous_release}/stable"
return f"{self.current_os_release}/stable"
return f"{self.o7k_release}/stable"

@property
def os_release_units(self) -> dict[OpenStackRelease, list[str]]:
def o7k_release_units(self) -> dict[OpenStackRelease, list[str]]:
"""Get the OpenStack release versions from the units.
:return: OpenStack release versions from the units.
:rtype: defaultdict[OpenStackRelease, list[str]]
"""
os_versions = defaultdict(list)
o7k_versions = defaultdict(list)
for unit in self.units.values():
os_version = self.get_latest_os_version(unit)
os_versions[os_version].append(unit.name)
o7k_version = self.get_latest_o7k_version(unit)
o7k_versions[o7k_version].append(unit.name)

return dict(os_versions)
return dict(o7k_versions)

@property
def need_crossgrade(self) -> bool:
Expand All @@ -290,7 +298,7 @@ def is_valid_track(self, charm_channel: str) -> bool:
except ValueError:
return False

def get_latest_os_version(self, unit: Unit) -> OpenStackRelease:
def get_latest_o7k_version(self, unit: Unit) -> OpenStackRelease:
"""Get the latest compatible OpenStack release based on the unit workload version.
:param unit: Unit
Expand All @@ -300,16 +308,16 @@ def get_latest_os_version(self, unit: Unit) -> OpenStackRelease:
:raises ApplicationError: When there are no compatible OpenStack release for the
workload version.
"""
compatible_os_versions = OpenStackCodenameLookup.find_compatible_versions(
compatible_o7k_versions = OpenStackCodenameLookup.find_compatible_versions(
self.charm, unit.workload_version
)
if not compatible_os_versions:
if not compatible_o7k_versions:
raise ApplicationError(
f"'{self.name}' with workload version {unit.workload_version} has no "
"compatible OpenStack release."
)

return max(compatible_os_versions)
return max(compatible_o7k_versions)

@staticmethod
def _get_track_from_channel(charm_channel: str) -> str:
Expand Down Expand Up @@ -361,10 +369,10 @@ async def _verify_workload_upgrade(self, target: OpenStackRelease, units: list[U
units_not_upgraded = []
for unit in units:
workload_version = app_status.units[unit.name].workload_version
compatible_os_versions = OpenStackCodenameLookup.find_compatible_versions(
compatible_o7k_versions = OpenStackCodenameLookup.find_compatible_versions(
self.charm, workload_version
)
if target not in compatible_os_versions:
if target not in compatible_o7k_versions:
units_not_upgraded.append(unit.name)

if units_not_upgraded:
Expand Down Expand Up @@ -556,7 +564,10 @@ def _get_refresh_charm_step(self, target: OpenStackRelease) -> PreUpgradeStep:
if self.is_from_charm_store:
return self._get_charmhub_migration_step(target)
if self.channel == LATEST_STABLE:
return self._get_change_to_openstack_channels_step(target)
return self._get_change_channel_possible_downgrade_step(
target, self.expected_current_channel(target)
)

if self._need_current_channel_refresh(target):
return self._get_refresh_current_channel_step()
logger.info(
Expand All @@ -579,27 +590,32 @@ def _get_charmhub_migration_step(self, target: OpenStackRelease) -> PreUpgradeSt
),
)

def _get_change_to_openstack_channels_step(self, target: OpenStackRelease) -> PreUpgradeStep:
"""Get the step for changing to an OpenStack channel.
def _get_change_channel_possible_downgrade_step(
self, target: OpenStackRelease, channel: str
) -> PreUpgradeStep:
"""Get the step for changing to a channel that can be a downgrade.
:param target: OpenStack release as target to upgrade.
:param target: OpenStack release as target to upgrade
:type target: OpenStackRelease
:return: Step for changing to OpenStack channels
:param channel: channel to upgrade
:type channel: str
:return: Step for possible downgrade.
:rtype: PreUpgradeStep
"""
logger.warning(
"Changing '%s' channel from %s to %s to upgrade to %s. This may be a charm downgrade, "
"which is generally not supported.",
self.name,
self.channel,
self.expected_current_channel(target),
channel,
target,
)
return PreUpgradeStep(
description = (
f"WARNING: Changing '{self.name}' channel from {self.channel} to "
f"{self.expected_current_channel(target)}. This may be a charm downgrade, "
"which is generally not supported.",
coro=self.model.upgrade_charm(self.name, self.expected_current_channel(target)),
f"{channel}. This may be a charm downgrade, which is generally not supported."
)
return PreUpgradeStep(
description=description, coro=self.model.upgrade_charm(self.name, channel)
)

def _get_refresh_current_channel_step(self) -> PreUpgradeStep:
Expand All @@ -621,23 +637,38 @@ def _need_current_channel_refresh(self, target: OpenStackRelease) -> bool:
:return: True if needs to refresh, False otherwise
:rtype: bool
"""
return bool(self.can_upgrade_to) and self.channel_codename <= target
return bool(self.can_upgrade_to) and self.channel_o7k_release <= target

def _get_upgrade_charm_step(self, target: OpenStackRelease) -> UpgradeStep:
"""Get step for upgrading the charm.
:param target: OpenStack release as target to upgrade.
:type target: OpenStackRelease
:raises ApplicationError: When the current channel is ahead from expected and the target.
:return: Step for upgrading the charm.
:rtype: UpgradeStep
"""
if self.channel != self.target_channel(target):
channel = self.expected_current_channel(target) if self.need_crossgrade else self.channel

if channel == self.target_channel(target):
logger.debug("%s channel already set to %s", self.name, self.channel)
return UpgradeStep()

# Normally, prior the upgrade the channel is equal to the application release.
# However, when colocated with other app, the channel can be in a release lesser than the
# workload version of the application.
if self.channel_o7k_release <= self.o7k_release:
return UpgradeStep(
description=f"Upgrade '{self.name}' to the new channel: "
description=f"Upgrade '{self.name}' from '{channel}' to the new channel: "
f"'{self.target_channel(target)}'",
coro=self.model.upgrade_charm(self.name, self.target_channel(target)),
)
return UpgradeStep()

raise ApplicationError(
f"The '{self.name}' application is using channel '{self.channel}'. Channels supported "
f"during this transition: '{self.expected_current_channel(target)}', "
f"'{self.target_channel(target)}'. Manual intervention is required."
)

def _set_action_managed_upgrade(self, enable: bool) -> UpgradeStep:
"""Set action-managed-upgrade config option.
Expand Down Expand Up @@ -724,7 +755,7 @@ def _get_change_install_repository_step(self, target: OpenStackRelease) -> Upgra
:return: Workload upgrade step
:rtype: UpgradeStep
"""
if self.os_origin != self.new_origin(target) and self.origin_setting:
if self.o7k_origin != self.new_origin(target) and self.origin_setting:
return UpgradeStep(
f"Change charm config of '{self.name}' '{self.origin_setting}' to "
f"'{self.new_origin(target)}'",
Expand Down Expand Up @@ -830,14 +861,14 @@ def _check_application_target(self, target: OpenStackRelease) -> None:
:raises HaltUpgradePlanGeneration: When the application halt the upgrade plan generation.
"""
logger.debug(
"%s application current os_release is %s and apt source is %s",
"%s application current o7k_release is %s and apt source is %s",
self.name,
self.current_os_release,
self.o7k_release,
self.apt_source_codename,
)

if (
self.current_os_release >= target
self.o7k_release >= target
and not self.can_upgrade_to
and self.apt_source_codename >= target
):
Expand Down Expand Up @@ -867,11 +898,11 @@ def _check_mismatched_versions(self, units: Optional[list[Unit]]) -> None:
):
return

os_versions = self.os_release_units
if len(os_versions.keys()) > 1:
o7k_versions = self.o7k_release_units
if len(o7k_versions.keys()) > 1:
mismatched_repr = [
f"'{openstack_release.codename}': {units}"
for openstack_release, units in os_versions.items()
for openstack_release, units in o7k_versions.items()
]

raise MismatchedOpenStackVersions(
Expand Down
Loading

0 comments on commit 1265215

Please sign in to comment.