Patches from current release in unavailable state

This change:
1) Allow a patch to be applied on top of an unavailable patch
2) Does not allow the system to go back to an unavailable patch
3) Does not raise obsolete alarm for unavailable patches in the
   current major release
4) After applying a patch on top of an unavailable patch, remove all
   the unavailable patches
5) Add the prepatched_iso and remove the requires tags from the lowest
   sysroot deployed patch

Test-Plan:
PASS: If a patch from the current major release is unavailable,
      does not raise the obsolete release alarm
PASS: Apply a patch on top of an unavailable patch
      - Check the deployment entity is correct
      - Check the ostree commit is correct
      - Check the unavailable patch was deleted
      - Check the lowest applied patch contains the prepatched_iso
        and no requires tags in the metadata
PASS: Should not allow removing a patch on top of an unavailable patch
PASS: Should not allow removal of an unavailable patch
PASS: Upgrade from 22.12 to 24.09.300 (with changes)

Closes-Bug: 2119980

Change-Id: Iaf83a55af4c33a9d38eb569c7d614662d60d7a36
Signed-off-by: Lindley Vieira <lindley.vieira@windriver.com>
This commit is contained in:
Lindley Vieira
2025-08-07 15:31:45 -04:00
parent 058d1a8efb
commit 7fb916cbd5
5 changed files with 75 additions and 18 deletions

View File

@@ -1,5 +1,5 @@
"""
Copyright (c) 2014-2024 Wind River Systems, Inc.
Copyright (c) 2014-2025 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
@@ -22,6 +22,8 @@ import software.config as cfg
from software.constants import ENABLE_DEV_CERTIFICATE_PATCH_IDENTIFIER
from software.software_functions import configure_logging
from software.software_functions import LOG
from software.software_functions import SW_VERSION
from software.utils import get_major_release_version
###################
# CONSTANTS
@@ -90,7 +92,7 @@ class PatchAlarmDaemon(object):
self._handle_patch_alarms()
self._get_handle_failed_hosts()
def _handle_patch_alarms(self):
def _handle_patch_alarms(self): # pylint: disable=too-many-branches
url = "http://%s/v1/release" % self.api_addr
try:
@@ -112,6 +114,9 @@ class PatchAlarmDaemon(object):
raise_dip_alarm = True
elif rel_metadata['state'] == 'unavailable':
raise_obs_alarm = True
if 'sw_version' in rel_metadata and \
get_major_release_version(rel_metadata['sw_version']) == SW_VERSION:
raise_obs_alarm = False
if 'release_id' in rel_metadata and ENABLE_DEV_CERTIFICATE_PATCH_IDENTIFIER in rel_metadata['release_id']:
raise_cert_alarm = True

View File

@@ -3,7 +3,7 @@
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2023-2024 Wind River Systems, Inc.
# Copyright (c) 2023-2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@@ -32,6 +32,7 @@ RC_SUCCESS = 0
RC_UNHEALTHY = 3
STATE_AVAILABLE = 'available'
STATE_DEPLOYED = 'deployed'
STATE_UNAVAILABLE = 'unavailable'
SYSTEM_MODE_SIMPLEX = "simplex"
# the space needed is derived from the sum of snapshot sizes
# defined in lvm_snapshot.LvmSnapshotManager.LOGICAL_VOLUMES
@@ -88,18 +89,19 @@ class HealthCheck(object):
# and implement patch precheck for subcloud
def _check_required_patches_state(self, required_patches, allow_available=False):
"""
Check if the required patches are in 'deployed' state, if allow_available is
True, the required_patches can be in 'available' state as well.
Check if the required patches are in 'deployed' or 'unavailable' state,
if allow_available is True, the required_patches can be in 'available' state as well.
:param required_patches: list of patches to be checked
:param allow_available: boolean if allow available patches
:return: boolean indicating success/failure and list of patches
that are not in the 'deployed' or 'available' state
that are not in the 'deployed', 'unavailable' or 'available' state
"""
success = True
releases = self._config.get("releases", "")
releases_in_allowed_states = []
for release in json.loads(releases):
if release['state'] == STATE_DEPLOYED or \
release['state'] == STATE_UNAVAILABLE or \
(allow_available and release['state'] == STATE_AVAILABLE):
releases_in_allowed_states.append(release)
@@ -413,7 +415,7 @@ class PatchHealthCheck(HealthCheck):
health_ok = True
output = ""
allow_available_patches = True
states_required = "deployed or available"
states_required = "deployed, unavailable or available"
# check required patches for target release
required_patches = self._get_required_patches()
@@ -421,7 +423,7 @@ class PatchHealthCheck(HealthCheck):
# 24.09.300 patch requires all previous patches to be deployed
if self._target_release == "24.09.300":
allow_available_patches = False
states_required = "deployed"
states_required = "deployed or unavailable"
success, missing_patches = self._check_required_patches_state(required_patches, allow_available_patches)
output += 'Required patches are %s: [%s]\n' \

View File

@@ -222,6 +222,8 @@ COMMIT_TAG = "commit"
COMMIT1_TAG = "commit1"
NUMBER_OF_COMMITS_TAG = "number_of_commits"
OSTREE_TAG = "ostree"
PREPATCHED_ISO_TAG = "prepatched_iso"
REQUIRES_TAG = "requires"
# Flags
INSTALL_LOCAL_FLAG = "/opt/software/.install_local"

View File

@@ -1,7 +1,7 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (c) 2024 Wind River Systems, Inc.
# Copyright (c) 2024-2025 Wind River Systems, Inc.
#
import os
@@ -299,9 +299,10 @@ class SWReleaseCollection(object):
@property
def running_release(self):
latest = None
for rel in self.iterate_releases_by_state(states.DEPLOYED):
if latest is None or rel.version_obj > latest.version_obj:
latest = rel
for state in (states.DEPLOYED, states.UNAVAILABLE):
for rel in self.iterate_releases_by_state(state):
if latest is None or rel.version_obj > latest.version_obj:
latest = rel
return latest

View File

@@ -2019,11 +2019,11 @@ class PatchController(PatchService):
:return: List of releases in the order for applying
"""
deployed_releases_id = []
deployed_or_unavailable_releases_id = []
preinstalled_patches = []
for rel in self.release_collection.iterate_releases():
if rel.state == states.DEPLOYED:
deployed_releases_id.append(rel.id)
if rel.state == states.DEPLOYED or rel.state == states.UNAVAILABLE:
deployed_or_unavailable_releases_id.append(rel.id)
if rel.prepatched_iso:
preinstalled_patches = rel.preinstalled_patches
@@ -2037,7 +2037,7 @@ class PatchController(PatchService):
to_apply_releases = [
rel_id for rel_id in release_dependencies
if f"-{running_release_sw_version}." in rel_id and
rel_id not in deployed_releases_id + preinstalled_patches
rel_id not in deployed_or_unavailable_releases_id + preinstalled_patches
]
to_apply_releases.sort()
@@ -3450,6 +3450,12 @@ class PatchController(PatchService):
"'software deploy show'\n" % deployment_list
elif operation == constants.REMOVE:
if deploy_release.state == states.UNAVAILABLE:
msg = "Cannot go back to an unavailable release"
LOG.error(msg)
msg_error += msg + "\n"
return dict(info=msg_info, warning=msg_warning, error=msg_error)
collect_current_load_for_hosts(deploy_sw_version, hostname=hostname)
create_deploy_hosts(hostname=hostname)
deployment_list = self.release_remove_order(deployment, running_release.id, running_release.sw_version)
@@ -3477,8 +3483,8 @@ class PatchController(PatchService):
LOG.error(msg)
msg_error += msg + "\n"
unremovable_verification = False
elif release.state == states.COMMITTED:
msg = "Release %s is committed and cannot be removed" % release_id
elif release.state == states.COMMITTED or release.state == states.UNAVAILABLE:
msg = "Release %s is %s and cannot be removed" % (release_id, release.state)
LOG.error(msg)
msg_error += msg + "\n"
unremovable_verification = False
@@ -3697,6 +3703,47 @@ class PatchController(PatchService):
if release.sw_version == major_release:
unavailable_releases.append(release.id)
ReleaseState(release_ids=unavailable_releases).replaced()
else:
# After deploy a patch, delete previous unavailable releases
# from current major release, if they exist (subcloud prestage case)
unavailable_releases = list(self.release_collection.iterate_releases_by_state(states.UNAVAILABLE))
if unavailable_releases:
for unavailable in unavailable_releases:
if unavailable.sw_version == major_release:
LOG.info("Deleting unavailable %s from current major release" % unavailable.id)
try:
os.remove("%s/%s-metadata.xml" % (states.UNAVAILABLE_DIR, unavailable.id))
except OSError:
LOG.error("Error when deleting %s metadata")
reload_release_data()
# Change the lowest deployed release metadata to prepatched
# Applied releases are all from current major release wich commits are deployed in sysroot
applied_releases_states = {states.DEPLOYED, states.DEPLOYING, states.COMMITTED}
for release in self.release_collection.iterate_releases():
if release.sw_version == major_release and release.state in applied_releases_states:
LOG.info("Changing %s metadata to prepatched" % release.id)
metadata_dir = states.RELEASE_STATE_TO_DIR_MAP[release.state]
metadata_file = "%s/%s-metadata.xml" % (metadata_dir, release.id)
tree = ET.parse(metadata_file)
root = tree.getroot()
self.add_text_tag_to_xml(root, constants.PREPATCHED_ISO_TAG, "Y")
requires_tag = root.find(constants.REQUIRES_TAG)
if requires_tag is not None:
root.remove(requires_tag)
ET.indent(tree, ' ')
with open(metadata_file, "wb") as outfile:
tree = ET.tostring(root)
outfile.write(tree)
# Run just for the first (lowest) release
break
# Set deploying releases to deployed state.
deploying_release_state.deploy_completed()