From 1d228bab28984a3fc4a354a7728615a059104d81 Mon Sep 17 00:00:00 2001 From: Igor Soares Date: Tue, 9 Apr 2024 19:34:52 -0300 Subject: [PATCH] Introduce multi-version auto downgrade for apps Introduce automatic downgrade of StarlingX applications to the multiple application version feature. Auto downgrades are triggered by default in scenarios which the applied application bundle is not available anymore under the applications folder but an older version of the same app is. For instance, when platform patches are removed and a previously available ostree is deployed, thus restoring the old set of available apps under the /usr/local/share/applications/helm/ directory. A new section called 'downgrades' can be added to the metadata.yaml file to disable the default behavior. For example: downgrades: auto_downgrade: false When auto downgrades are disabled the current applied version remains unchanged. Test plan: PASS: build-pkgs -a && build-image PASS: AIO-SX fresh install. PASS: Apply platform-integ-apps. Update platform-integ-apps using a tarball that is not available under /usr/local/share/applications/helm/ and that does not contain the downgrade section. Confirm that platform-integ-apps is downgraded. PASS: Apply platform-integ-apps. Update platform-integ-apps using a tarball that is not available under /usr/local/share/applications/helm/ and that has the auto_downgrade metadata option set to 'true'. Confirm that platform-integ-apps is downgraded. PASS: Apply platform-integ-apps. Update platform-integ-apps using a tarball that is not available under /usr/local/share/applications/helm/ and that has the auto_downgrade metadata option set to 'false'. Confirm that the originally applied platform-integ-apps version remains unchanged. PASS: Run a kubernetes upgrade with apps to be pre and post updated. Confirm that apps are successfully updated and not downgraded after the Kubernetes upgrade has finished. Story: 2010929 Task: 49847 Change-Id: I33f0e0a5b8db128aef76fb93ba322364881097cf Signed-off-by: Igor Soares --- .../sysinv/sysinv/common/app_metadata.py | 9 +++- .../sysinv/sysinv/sysinv/common/constants.py | 3 ++ .../sysinv/sysinv/sysinv/conductor/manager.py | 41 +++++++++++++++---- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py b/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py index 686032344c..a82de4b6b6 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py +++ b/sysinv/sysinv/sysinv/sysinv/common/app_metadata.py @@ -125,7 +125,7 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None): if not error_message: error_message = _("Invalid boolean value: {}" .format(value)) - raise exception.SysinvException(error_message) + raise exception.SysinvException(error_message) def validate_dict(value, error_message=None): """Validate dictionary types""" @@ -381,6 +381,13 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None): for version in from_versions: validate_string(version) + # Downgrades section validation + downgrades = validate_dict_field(doc, constants.APP_METADATA_DOWNGRADES) + if downgrades: + validate_boolstr_field( + downgrades, + constants.APP_METADATA_AUTO_DOWNGRADE) + # Kubernetes version section validation k8s_version = validate_k8s_version(doc) if k8s_version: diff --git a/sysinv/sysinv/sysinv/sysinv/common/constants.py b/sysinv/sysinv/sysinv/sysinv/common/constants.py index e6aad8cb51..ed22d1ae18 100644 --- a/sysinv/sysinv/sysinv/sysinv/common/constants.py +++ b/sysinv/sysinv/sysinv/sysinv/common/constants.py @@ -1982,6 +1982,9 @@ APP_METADATA_UPGRADES = 'upgrades' APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY = 'update_failure_no_rollback' APP_METADATA_AUTO_UPDATE = 'auto_update' APP_METADATA_AUTO_UPDATE_DEFAULT_VALUE = True +APP_METADATA_DOWNGRADES = 'downgrades' +APP_METADATA_AUTO_DOWNGRADE = 'auto_downgrade' +APP_METADATA_AUTO_DOWNGRADE_DEFAULT_VALUE = True APP_METADATA_FAILED_VERSIONS = 'failed_versions' APP_METADATA_FROM_VERSIONS = 'from_versions' APP_METADATA_SUPPORTED_K8S_VERSION = 'supported_k8s_version' diff --git a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py index e6626f05e1..8536a32989 100644 --- a/sysinv/sysinv/sysinv/sysinv/conductor/manager.py +++ b/sysinv/sysinv/sysinv/sysinv/conductor/manager.py @@ -53,6 +53,7 @@ import xml.etree.ElementTree as ElementTree from contextlib import contextmanager from datetime import datetime from datetime import timedelta +from distutils.util import strtobool from distutils.version import LooseVersion from copy import deepcopy from urllib3.exceptions import MaxRetryError @@ -7782,6 +7783,11 @@ class ConductorManager(service.PeriodicService): True, k8s_upgrade_timing) + auto_downgrade = strtobool(app.app_metadata.get(constants.APP_METADATA_DOWNGRADES, {}) + .get(constants.APP_METADATA_AUTO_DOWNGRADE, + str(constants.APP_METADATA_AUTO_DOWNGRADE_DEFAULT_VALUE))) + latest_downgrade_bundle = None + available_versions = set() latest_version_bundle = None if k8s_version is None: @@ -7790,15 +7796,8 @@ class ConductorManager(service.PeriodicService): k8s_version = k8s_version.strip().lstrip('v') for bundle_metadata in bundle_metadata_list: - if LooseVersion(bundle_metadata.version) <= LooseVersion(app.app_version): - LOG.debug("Bundle {} version {} lower than installed app version ({})" - .format(bundle_metadata.file_path, - bundle_metadata.version, - app.app_version)) - elif not bundle_metadata.auto_update: - LOG.debug("Application auto update disabled for bundle {}" - .format(bundle_metadata.file_path)) - elif LooseVersion(k8s_version) < LooseVersion(bundle_metadata.k8s_minimum_version): + available_versions.add(bundle_metadata.version) + if LooseVersion(k8s_version) < LooseVersion(bundle_metadata.k8s_minimum_version): LOG.debug("Kubernetes version {} is lower than {} which is " "the minimum required for bundle {}" .format(k8s_version, @@ -7811,6 +7810,21 @@ class ConductorManager(service.PeriodicService): .format(k8s_version, bundle_metadata.k8s_maximum_version, bundle_metadata.file_path)) + elif LooseVersion(bundle_metadata.version) == LooseVersion(app.app_version): + LOG.debug("Bundle {} version and installed app version are the same ({})" + .format(bundle_metadata.file_path, + app.app_version)) + elif LooseVersion(bundle_metadata.version) < LooseVersion(app.app_version): + LOG.debug("Bundle {} version {} is lower than installed app version ({})" + .format(bundle_metadata.file_path, + bundle_metadata.version, + app.app_version)) + if (latest_downgrade_bundle is None or LooseVersion(bundle_metadata.version) > + LooseVersion(latest_downgrade_bundle.version)): + latest_downgrade_bundle = bundle_metadata + elif not bundle_metadata.auto_update: + LOG.debug("Application auto update disabled for bundle {}" + .format(bundle_metadata.file_path)) elif ((latest_version_bundle is None) or (LooseVersion(bundle_metadata.version) > LooseVersion(latest_version_bundle.version))): @@ -7818,6 +7832,15 @@ class ConductorManager(service.PeriodicService): # of the current one is higher than the one previously set. latest_version_bundle = bundle_metadata + # Downgrade if the installed app version is not available anymore and an older compatible + # bundle is available instead. + if (auto_downgrade and + app.app_version not in available_versions and + latest_downgrade_bundle is not None): + LOG.info("Application {} will be downgraded from version {} to {}" + .format(app.name, app.app_version, latest_downgrade_bundle.version)) + return latest_downgrade_bundle + return latest_version_bundle def _auto_update_app(self,