From e8486a91d1fe88af93702f0bb65672045974a817 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Wed, 30 Apr 2025 08:40:55 -0700 Subject: [PATCH 01/13] AzureLinux: Strict SDP support --- src/core/src/core_logic/ExecutionConfig.py | 18 +++++-- .../package_managers/TdnfPackageManager.py | 53 ++++++++++++++++--- src/core/tests/Test_TdnfPackageManager.py | 49 +++++++++++++++++ .../tests/library/LegacyEnvLayerExtensions.py | 2 + 4 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/core/src/core_logic/ExecutionConfig.py b/src/core/src/core_logic/ExecutionConfig.py index 04cec562..35d35eca 100644 --- a/src/core/src/core_logic/ExecutionConfig.py +++ b/src/core/src/core_logic/ExecutionConfig.py @@ -107,14 +107,24 @@ def __get_max_patch_publish_date(self, health_store_id): # type: (str) -> object """ Obtains implicit date ceiling for published date - converts pub_off_sku_2024.04.01 to 20240401T000000Z """ max_patch_publish_date = str() - if health_store_id is not None and health_store_id != "": - date_candidate = str(health_store_id)[-10:].replace(".", "") # last 10 characters and remove '.' - if len(date_candidate) == 8 and date_candidate.isdigit() and str(health_store_id)[-11:-10] == "_": - max_patch_publish_date = date_candidate + "T000000Z" + date_in_health_store_id = self.validate_date_in_health_store_id(health_store_id) + if date_in_health_store_id != "": + max_patch_publish_date = date_in_health_store_id + "T000000Z" self.composite_logger.log_debug("[EC] Getting max patch publish date. [MaxPatchPublishDate={0}][HealthStoreId={1}]".format(str(max_patch_publish_date), str(health_store_id))) return max_patch_publish_date + @staticmethod + def validate_date_in_health_store_id(health_store_id): + # type: (str) -> (str) + """ Verifies if health_store_id contains an acceptable date i.e. Validates 2024.04.01 is an acceptable date format if health_store_id=pub_off_sku_2024.04.01 """ + """ Returns date in format: %Y%m%d i.e 20240401 in the above example """ + if health_store_id is not None and health_store_id != "": + date_candidate = str(health_store_id)[-10:].replace(".", "") # last 10 characters and remove '.' + if len(date_candidate) == 8 and date_candidate.isdigit() and str(health_store_id)[-11:-10] == "_": + return date_candidate + return "" + def __get_max_patch_publish_date_from_inclusions(self, included_package_name_mask_list): # type (str) -> str # This is for AzGPS mitigation mode execution for Strict safe-deployment of patches. diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 0be3cfa9..f0633303 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -15,9 +15,11 @@ # Requires Python 2.7+ """TdnfPackageManager for Azure Linux""" +import datetime import json import os import re +import time from core.src.core_logic.VersionComparator import VersionComparator from core.src.package_managers.PackageManager import PackageManager @@ -33,14 +35,17 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.cmd_clean_cache = "sudo tdnf clean expire-cache" self.cmd_repo_refresh = "sudo tdnf -q list updates" + # fetch snapshottime from health_store_id + self.health_store_id_in_posix_time = self.__get_posix_time_from_health_store_id(execution_config) + # Support to get updates and their dependencies - self.tdnf_check = 'sudo tdnf -q list updates' - self.single_package_check_versions = 'sudo tdnf list available ' - self.single_package_check_installed = 'sudo tdnf list installed ' - self.single_package_upgrade_simulation_cmd = 'sudo tdnf install --assumeno --skip-broken ' + self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.health_store_id_in_posix_time) + self.single_package_check_versions = self.__generate_command_with_snapshottime('sudo tdnf list available ', self.health_store_id_in_posix_time) + self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.health_store_id_in_posix_time) + self.single_package_upgrade_simulation_cmd = self.__generate_command_with_snapshottime('sudo tdnf install --assumeno --skip-broken ', self.health_store_id_in_posix_time) # Install update - self.single_package_upgrade_cmd = 'sudo tdnf -y install --skip-broken ' + self.single_package_upgrade_cmd = self.__generate_command_with_snapshottime('sudo tdnf -y install --skip-broken ', self.health_store_id_in_posix_time) # Package manager exit code(s) self.tdnf_exitcode_ok = 0 @@ -89,6 +94,39 @@ def refresh_repo(self): self.invoke_package_manager(self.cmd_clean_cache) self.invoke_package_manager(self.cmd_repo_refresh) + # region Fetch SnapshotTime + def __get_posix_time_from_health_store_id(self, execution_config): + """Converts date str received (in format: yyyy.mm.dd) to POSIX time string""" + # eg: Input: 2024.12.20 (str) -> Output: 1734681600 (str) + posix_time = str() + health_store_id = execution_config.health_store_id + date_in_health_store_id = execution_config.validate_date_in_health_store_id(health_store_id) + + if date_in_health_store_id != "": + try: + posix_time = str(int(self.__string_to_posix_time(date_in_health_store_id, "%Y%m%d"))) + except Exception as error: + self.composite_logger.log_debug("[TDNF] Could not fetch POSIX time from health store id. [HealthStoreId={0}][Exception={1}]".format(str(health_store_id), repr(error))) + + self.composite_logger.log_debug("[TDNF] Getting POSIX time from health store id. [POSIXTime={0}][HealthStoreId={1}]".format(str(posix_time), str(health_store_id))) + return posix_time + + @staticmethod + def __string_to_posix_time(string, format_string): + datetime_object = datetime.datetime.strptime(string, format_string) + posix_timestamp = time.mktime(datetime_object.timetuple()) + return posix_timestamp + + @staticmethod + def __generate_command_with_snapshottime(command_template, snapshotposixtime=str()): + # type: (str, str) -> str + """ Prepares a standard command to use snapshottime.""" + if snapshotposixtime == str(): + return command_template.replace('', str()) + else: + return command_template.replace('', ('--snapshottime={0}'.format(str(snapshotposixtime)))) + # endregion + # region Get Available Updates def invoke_package_manager_advanced(self, command, raise_on_exception=True): """Get missing updates using the command input""" @@ -318,14 +356,15 @@ def is_arch_in_package_details(package_detail, package_arch_to_look_for): def get_dependent_list(self, packages): """Returns dependent List for the list of packages""" + cmd = self.single_package_upgrade_simulation_cmd package_names = "" for index, package in enumerate(packages): if index != 0: package_names += ' ' package_names += package - self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(self.single_package_upgrade_simulation_cmd + package_names))) - output = self.invoke_package_manager(self.single_package_upgrade_simulation_cmd + package_names) + self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(cmd + package_names))) + output = self.invoke_package_manager(cmd + package_names) dependencies = self.extract_dependencies(output, packages) self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Packages={0}][DependencyCount={1}]".format(str(packages), len(dependencies))) return dependencies diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index ddaa9852..3b3ef9b9 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -41,6 +41,55 @@ def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode= raise Exception # endregion + def test_invalid_health_store_id_posix_time(self): + # health_store_id is None + self.runtime.stop() + argument_composer = ArgumentComposer() + argument_composer.health_store_id = None + self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + self.container = self.runtime.container + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + + # health_store_id is empty string + self.runtime.stop() + argument_composer = ArgumentComposer() + argument_composer.health_store_id = "" + self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + self.container = self.runtime.container + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + + # health_store_id is random string + self.runtime.stop() + argument_composer = ArgumentComposer() + argument_composer.health_store_id = "pub_offer_sku_20.312.543" + self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + self.container = self.runtime.container + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + + def test_valid_health_store_id_posix_time(self): + # valid health_store_id + self.runtime.stop() + argument_composer = ArgumentComposer() + argument_composer.health_store_id = "pub_offer_sku_2024.04.01" + self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + self.container = self.runtime.container + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.health_store_id_in_posix_time is not None) + self.assertEqual("1711954800", package_manager.health_store_id_in_posix_time) + + # health_store_id is in unexpected format + self.runtime.stop() + argument_composer = ArgumentComposer() + argument_composer.health_store_id = "pub_offer_sk_u_2024.04.01" + self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + self.container = self.runtime.container + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager.health_store_id_in_posix_time is not None) + self.assertEqual("1711954800", package_manager.health_store_id_in_posix_time) + def test_do_processes_require_restart(self): """Unit test for tdnf package manager""" # Restart required diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index 1a1cf24f..3b641caf 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -14,6 +14,7 @@ # # Requires Python 2.7+ import os +import re import sys from core.src.bootstrap.Constants import Constants @@ -606,6 +607,7 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): "Error(1032) : Operation aborted.\n" elif cmd.find("list installed") > -1: code = 0 + cmd = re.sub(r"--snapshottime=\d+", '', cmd) package = cmd.replace('sudo tdnf list installed ', '') whitelisted_versions = [ '3.0-16.azl3', '3.0-3.azl3', '2.5.4-1.azl3', '3.12.3-6.azl3', '2.11.5-1.azl3', '102-7.azl3', '6.6.78.1-1.azl3'] # any list of versions you want to work for *any* package From a592072ffe346608dce28b5baa6e5c62da58db76 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Wed, 30 Apr 2025 16:29:42 -0700 Subject: [PATCH 02/13] AzureLinux Strict SDP support: Addressing PR feedback #1 --- src/core/src/bootstrap/EnvLayer.py | 8 ++++ src/core/src/core_logic/ExecutionConfig.py | 18 ++------ .../package_managers/TdnfPackageManager.py | 42 ++++++++----------- src/core/tests/Test_TdnfPackageManager.py | 18 ++++---- 4 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 1b351d92..37d5dd62 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -534,6 +534,14 @@ def utc_to_standard_datetime(utc_datetime): def standard_datetime_to_utc(std_datetime): """ Converts datetime object to string of format '"%Y-%m-%dT%H:%M:%SZ"' """ return std_datetime.strftime("%Y-%m-%dT%H:%M:%SZ") + + @staticmethod + def datetime_string_to_posix_time(datetime_string, format_string): + """ Converts string of given format to posix datetime string. """ + # eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str) + datetime_object = datetime.datetime.strptime(datetime_string, format_string) + posix_timestamp = str(int(time.mktime(datetime_object.timetuple()))) + return posix_timestamp # endregion - DateTime emulator and extensions # region - Core Emulator support functions diff --git a/src/core/src/core_logic/ExecutionConfig.py b/src/core/src/core_logic/ExecutionConfig.py index 35d35eca..04cec562 100644 --- a/src/core/src/core_logic/ExecutionConfig.py +++ b/src/core/src/core_logic/ExecutionConfig.py @@ -107,23 +107,13 @@ def __get_max_patch_publish_date(self, health_store_id): # type: (str) -> object """ Obtains implicit date ceiling for published date - converts pub_off_sku_2024.04.01 to 20240401T000000Z """ max_patch_publish_date = str() - date_in_health_store_id = self.validate_date_in_health_store_id(health_store_id) - if date_in_health_store_id != "": - max_patch_publish_date = date_in_health_store_id + "T000000Z" - - self.composite_logger.log_debug("[EC] Getting max patch publish date. [MaxPatchPublishDate={0}][HealthStoreId={1}]".format(str(max_patch_publish_date), str(health_store_id))) - return max_patch_publish_date - - @staticmethod - def validate_date_in_health_store_id(health_store_id): - # type: (str) -> (str) - """ Verifies if health_store_id contains an acceptable date i.e. Validates 2024.04.01 is an acceptable date format if health_store_id=pub_off_sku_2024.04.01 """ - """ Returns date in format: %Y%m%d i.e 20240401 in the above example """ if health_store_id is not None and health_store_id != "": date_candidate = str(health_store_id)[-10:].replace(".", "") # last 10 characters and remove '.' if len(date_candidate) == 8 and date_candidate.isdigit() and str(health_store_id)[-11:-10] == "_": - return date_candidate - return "" + max_patch_publish_date = date_candidate + "T000000Z" + + self.composite_logger.log_debug("[EC] Getting max patch publish date. [MaxPatchPublishDate={0}][HealthStoreId={1}]".format(str(max_patch_publish_date), str(health_store_id))) + return max_patch_publish_date def __get_max_patch_publish_date_from_inclusions(self, included_package_name_mask_list): # type (str) -> str diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index f0633303..7357242e 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -36,16 +36,16 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.cmd_repo_refresh = "sudo tdnf -q list updates" # fetch snapshottime from health_store_id - self.health_store_id_in_posix_time = self.__get_posix_time_from_health_store_id(execution_config) + self.snapshot_posix_time = self.__get_posix_time(execution_config.max_patch_publish_date, env_layer) # Support to get updates and their dependencies - self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.health_store_id_in_posix_time) - self.single_package_check_versions = self.__generate_command_with_snapshottime('sudo tdnf list available ', self.health_store_id_in_posix_time) - self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.health_store_id_in_posix_time) - self.single_package_upgrade_simulation_cmd = self.__generate_command_with_snapshottime('sudo tdnf install --assumeno --skip-broken ', self.health_store_id_in_posix_time) + self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.snapshot_posix_time) + self.single_package_check_versions = self.__generate_command_with_snapshottime('sudo tdnf list available ', self.snapshot_posix_time) + self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.snapshot_posix_time) + self.single_package_upgrade_simulation_cmd = self.__generate_command_with_snapshottime('sudo tdnf install --assumeno --skip-broken ', self.snapshot_posix_time) # Install update - self.single_package_upgrade_cmd = self.__generate_command_with_snapshottime('sudo tdnf -y install --skip-broken ', self.health_store_id_in_posix_time) + self.single_package_upgrade_cmd = self.__generate_command_with_snapshottime('sudo tdnf -y install --skip-broken ', self.snapshot_posix_time) # Package manager exit code(s) self.tdnf_exitcode_ok = 0 @@ -94,29 +94,21 @@ def refresh_repo(self): self.invoke_package_manager(self.cmd_clean_cache) self.invoke_package_manager(self.cmd_repo_refresh) - # region Fetch SnapshotTime - def __get_posix_time_from_health_store_id(self, execution_config): - """Converts date str received (in format: yyyy.mm.dd) to POSIX time string""" - # eg: Input: 2024.12.20 (str) -> Output: 1734681600 (str) + # region Strict SDP using SnapshotTime + def __get_posix_time(self, datetime_to_convert, env_layer): + """Converts date str received to POSIX time string""" posix_time = str() - health_store_id = execution_config.health_store_id - date_in_health_store_id = execution_config.validate_date_in_health_store_id(health_store_id) - - if date_in_health_store_id != "": - try: - posix_time = str(int(self.__string_to_posix_time(date_in_health_store_id, "%Y%m%d"))) - except Exception as error: - self.composite_logger.log_debug("[TDNF] Could not fetch POSIX time from health store id. [HealthStoreId={0}][Exception={1}]".format(str(health_store_id), repr(error))) + datetime_to_convert_format = '%Y%m%dT%H%M%SZ' + self.composite_logger.log_debug("[TDNF] Getting POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}]".format(str(datetime_to_convert), datetime_to_convert_format)) + try: + if datetime_to_convert != str(): + posix_time = env_layer.datetime.datetime_string_to_posix_time(datetime_to_convert, datetime_to_convert_format) + except Exception as error: + self.composite_logger.log_debug("[TDNF] Could not fetch POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}][ComputedPosixTime={2}][Error={3}]".format(str(datetime_to_convert), datetime_to_convert_format, posix_time, repr(error))) - self.composite_logger.log_debug("[TDNF] Getting POSIX time from health store id. [POSIXTime={0}][HealthStoreId={1}]".format(str(posix_time), str(health_store_id))) + self.composite_logger.log_debug("[TDNF] Computed POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}][ComputedPosixTime={2}]".format(str(datetime_to_convert), datetime_to_convert_format, posix_time)) return posix_time - @staticmethod - def __string_to_posix_time(string, format_string): - datetime_object = datetime.datetime.strptime(string, format_string) - posix_timestamp = time.mktime(datetime_object.timetuple()) - return posix_timestamp - @staticmethod def __generate_command_with_snapshottime(command_template, snapshotposixtime=str()): # type: (str, str) -> str diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index 3b3ef9b9..fd0080a6 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -41,7 +41,7 @@ def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode= raise Exception # endregion - def test_invalid_health_store_id_posix_time(self): + def test_invalid_datetime_to_convert_to_posix_time(self): # health_store_id is None self.runtime.stop() argument_composer = ArgumentComposer() @@ -49,7 +49,7 @@ def test_invalid_health_store_id_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + self.assertTrue(package_manager.snapshot_posix_time is str()) # health_store_id is empty string self.runtime.stop() @@ -58,7 +58,7 @@ def test_invalid_health_store_id_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + self.assertTrue(package_manager.snapshot_posix_time is str()) # health_store_id is random string self.runtime.stop() @@ -67,9 +67,9 @@ def test_invalid_health_store_id_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.health_store_id_in_posix_time is str()) + self.assertTrue(package_manager.snapshot_posix_time is str()) - def test_valid_health_store_id_posix_time(self): + def test_valid_datetime_to_convert_to_posix_time(self): # valid health_store_id self.runtime.stop() argument_composer = ArgumentComposer() @@ -77,8 +77,8 @@ def test_valid_health_store_id_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.health_store_id_in_posix_time is not None) - self.assertEqual("1711954800", package_manager.health_store_id_in_posix_time) + self.assertTrue(package_manager.snapshot_posix_time is not None) + self.assertEqual("1711954800", package_manager.snapshot_posix_time) # health_store_id is in unexpected format self.runtime.stop() @@ -87,8 +87,8 @@ def test_valid_health_store_id_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.health_store_id_in_posix_time is not None) - self.assertEqual("1711954800", package_manager.health_store_id_in_posix_time) + self.assertTrue(package_manager.snapshot_posix_time is not None) + self.assertEqual("1711954800", package_manager.snapshot_posix_time) def test_do_processes_require_restart(self): """Unit test for tdnf package manager""" From 77f92d3f920e64997515f434417b0fde382c43fc Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 1 May 2025 08:16:10 -0700 Subject: [PATCH 03/13] AzureLinux Strict SDP support: Minor changes --- src/core/src/package_managers/TdnfPackageManager.py | 7 ++----- src/core/tests/Test_TdnfPackageManager.py | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 7357242e..5119e612 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -15,11 +15,9 @@ # Requires Python 2.7+ """TdnfPackageManager for Azure Linux""" -import datetime import json import os import re -import time from core.src.core_logic.VersionComparator import VersionComparator from core.src.package_managers.PackageManager import PackageManager @@ -348,15 +346,14 @@ def is_arch_in_package_details(package_detail, package_arch_to_look_for): def get_dependent_list(self, packages): """Returns dependent List for the list of packages""" - cmd = self.single_package_upgrade_simulation_cmd package_names = "" for index, package in enumerate(packages): if index != 0: package_names += ' ' package_names += package - self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(cmd + package_names))) - output = self.invoke_package_manager(cmd + package_names) + self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(self.single_package_upgrade_simulation_cmd + package_names))) + output = self.invoke_package_manager(self.single_package_upgrade_simulation_cmd + package_names) dependencies = self.extract_dependencies(output, packages) self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Packages={0}][DependencyCount={1}]".format(str(packages), len(dependencies))) return dependencies diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index fd0080a6..501e1d01 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -49,7 +49,7 @@ def test_invalid_datetime_to_convert_to_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.snapshot_posix_time is str()) + self.assertTrue(isinstance(package_manager.snapshot_posix_time, str)) # health_store_id is empty string self.runtime.stop() @@ -58,7 +58,7 @@ def test_invalid_datetime_to_convert_to_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.snapshot_posix_time is str()) + self.assertTrue(isinstance(package_manager.snapshot_posix_time, str)) # health_store_id is random string self.runtime.stop() @@ -67,7 +67,7 @@ def test_invalid_datetime_to_convert_to_posix_time(self): self.runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) self.container = self.runtime.container package_manager = self.container.get('package_manager') - self.assertTrue(package_manager.snapshot_posix_time is str()) + self.assertTrue(isinstance(package_manager.snapshot_posix_time, str)) def test_valid_datetime_to_convert_to_posix_time(self): # valid health_store_id From 5740839d9c4ca6d3843b1f2563d1f4c85d497cb3 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Tue, 13 May 2025 08:01:25 -0700 Subject: [PATCH 04/13] AzureLinux Strict SDP support: intermediary commit explaining approach --- src/core/src/bootstrap/EnvLayer.py | 5 +++++ src/core/src/package_managers/TdnfPackageManager.py | 11 +++++++++++ src/core/tests/Test_Bootstrapper.py | 4 ++-- src/core/tests/Test_EnvLayer.py | 1 + 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 798df580..c1bdcb79 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -241,6 +241,11 @@ def cpu_arch(): # architecture @staticmethod def vm_name(): # machine name return platform.node() + + @staticmethod + def version(): + return str() if (EnvLayer.get_python_major_version() == 2) else distro.os_release_attr('version') + # endregion - Platform extensions # region - File system extensions diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 12a98f0c..b68f296a 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -112,6 +112,17 @@ def __get_posix_time(self, datetime_to_convert, env_layer): def __generate_command_with_snapshottime(command_template, snapshotposixtime=str()): # type: (str, str) -> str """ Prepares a standard command to use snapshottime.""" + + # finds azlinux major version, and tdnf version + # if azlinux < 3.0.20241005 + # no snaphottime + # if azlinux >= 3.0.20241005 and tdnf < 3.5.8-3 + # 1 attempt to update tdnf + # if succeeds, add snapshottime + # if fails, no snapshottime + # if azlinux >= 3.0.20241005 and tdnf >= 3.5.8-3 + # add snapshottime + if snapshotposixtime == str(): return command_template.replace('', str()) else: diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index be8a64a8..4fd08723 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -28,8 +28,8 @@ class TestBootstrapper(unittest.TestCase): - def __init__(self, methodName: str = "runTest"): - super().__init__(methodName) + # def __init__(self, methodName: str = "runTest"): + # super().__init__(methodName) def setUp(self): self.sudo_check_status_attempts = 0 diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 24bf5c73..e9ccb598 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -108,6 +108,7 @@ def test_platform(self): self.envlayer.platform.os_type() self.envlayer.platform.cpu_arch() self.envlayer.platform.vm_name() + self.envlayer.platform.version() if __name__ == '__main__': From f27c3e3e0deacd1eb08d21cf047714f9376d02a2 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 19 Jun 2025 07:53:11 -0700 Subject: [PATCH 05/13] AzureLinux Strict SDP support: Added pseudo code to discuss approach --- src/core/src/bootstrap/Constants.py | 2 +- src/core/src/core_logic/PatchInstaller.py | 4 +++- .../package_managers/TdnfPackageManager.py | 24 ++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index ebaa1d78..1e672c37 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -347,7 +347,7 @@ class TelemetryTaskName(EnumBackport): TELEMETRY_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Azure Linux Agent version. To resolve: http://aka.ms/UpdateLinuxAgent" TELEMETRY_COMPATIBLE_MSG = "Minimum Azure Linux Agent version prerequisite met" PYTHON_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Python version. Minimum Python version required is 2.7. [DetectedPythonVersion={0}]" - INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" + INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" # TBD: the link is specific to Canonical, we need a generic one or one link of each distro UTC_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # EnvLayer Constants diff --git a/src/core/src/core_logic/PatchInstaller.py b/src/core/src/core_logic/PatchInstaller.py index 5af1b8df..92e9e478 100644 --- a/src/core/src/core_logic/PatchInstaller.py +++ b/src/core/src/core_logic/PatchInstaller.py @@ -170,6 +170,7 @@ def install_updates_azgps_coordinated(self, maintenance_window, package_manager, remaining_time = maintenance_window.get_remaining_time_in_minutes() try: + # TBD: Pseudo code for strict sdp: this will change and follow install_updates() logic to get_available_updates() and then use get_security_packages to only mark the packages as Security, This is needed in all distros that don't support package classifications all_packages, all_package_versions = package_manager.get_all_updates(cached=False) packages, package_versions = package_manager.get_security_updates() self.last_still_needed_packages = list(all_packages) @@ -191,7 +192,8 @@ def install_updates_azgps_coordinated(self, maintenance_window, package_manager, install_result = Constants.FAILED for i in range(0, Constants.MAX_INSTALLATION_RETRY_COUNT): - code, out = package_manager.install_security_updates_azgps_coordinated() + # TBD: Do we need batch processing in AzGPS-coordinated? No, as we are only installing security updates without passing in a list of packages. + code, out = package_manager.install_security_updates_azgps_coordinated() # TBD: this will change to pass in the compiled list of packages to update installed_update_count += self.perform_status_reconciliation_conditionally(package_manager) remaining_time = maintenance_window.get_remaining_time_in_minutes() diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index b68f296a..6000789d 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -35,11 +35,20 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ # fetch snapshottime from health_store_id self.snapshot_posix_time = self.__get_posix_time(execution_config.max_patch_publish_date, env_layer) + # Pseudo code for strict sdp + # if execution_config.operation.lower() == Constants.INSTALLATION.lower() and execution_config.max_patch_publish_date is not None and execution_config.health_store_id is not None: + # verify if VM matches config azlinux version and tdnf version + # if azlinux < 3.0.20241005 and tdnf < 3.5.8-3: + # self.snapshot_posix_time = Compute posixtime from execution_config.max_patch_publish_date // could throw an exception and fail the entire operation any issues with posix time computation + # else: + # Log and return unsupported error: VM config doesn't match the requirements for strict SDP + # else: + # self.snapshot_posix_time = str() # no snapshottime # Support to get updates and their dependencies - self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.snapshot_posix_time) + self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.snapshot_posix_time) # TBD: snapshottime doesn't work here and only returns the latest version from the timestamp it is valida for. For older snapshottime, it returns empty self.single_package_check_versions = self.__generate_command_with_snapshottime('sudo tdnf list available ', self.snapshot_posix_time) - self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.snapshot_posix_time) + self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.snapshot_posix_time) # doesn't need snapshottime self.single_package_upgrade_simulation_cmd = self.__generate_command_with_snapshottime('sudo tdnf install --assumeno --skip-broken ', self.snapshot_posix_time) # Install update @@ -111,8 +120,12 @@ def __get_posix_time(self, datetime_to_convert, env_layer): @staticmethod def __generate_command_with_snapshottime(command_template, snapshotposixtime=str()): # type: (str, str) -> str - """ Prepares a standard command to use snapshottime.""" + # Pseudo code for strict sdp + # generates commands with snapshottime if snapshot_posix_time is not empty and machine meets the requirements of Az Linux and TDNF versions + + """ Prepares a standard command to use snapshottime.""" + # Psuedo code for strict sdp: Oppurtunistic tdnf upgrade to supported version by AzGPS, good to have for later as an enhancement # finds azlinux major version, and tdnf version # if azlinux < 3.0.20241005 # no snaphottime @@ -158,6 +171,11 @@ def get_all_updates(self, cached=False): out = self.invoke_package_manager(self.tdnf_check) self.all_updates_cached, self.all_update_versions_cached = self.extract_packages_and_versions(out) + # Pseudo code for strict sdp: + # if self.snapshot_posix_time != str(): // OR execution_config.max_patch_publish_date is not None: + # run sudo tdnf list available --snapshottime= to get the list of available updates for that snapshottime + # For each package in all_updates_cached: + # Find it's corresponding entry in list available output and fetch the package version from it self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(False), len(self.all_updates_cached))) return self.all_updates_cached, self.all_update_versions_cached From 4b7504922c8bfbb57abdc9b9cb75e62ca93835ab Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 10 Jul 2025 06:01:16 -0700 Subject: [PATCH 06/13] AzureLinux Strict SDP support: Modifying strict sdp logic to follow canonical's approach --- src/core/src/core_logic/PatchInstaller.py | 5 +- .../package_managers/TdnfPackageManager.py | 71 +++++++------------ src/core/tests/Test_CoreMain.py | 8 +-- src/core/tests/Test_TdnfPackageManager.py | 6 +- 4 files changed, 33 insertions(+), 57 deletions(-) diff --git a/src/core/src/core_logic/PatchInstaller.py b/src/core/src/core_logic/PatchInstaller.py index 92e9e478..6247433f 100644 --- a/src/core/src/core_logic/PatchInstaller.py +++ b/src/core/src/core_logic/PatchInstaller.py @@ -79,6 +79,7 @@ def start_installation(self, simulate=False): if self.execution_config.max_patch_publish_date != str(): self.package_manager.set_max_patch_publish_date(self.execution_config.max_patch_publish_date) + # todo: validate if this meets the Az Linux and tdnf requirements if self.package_manager.max_patch_publish_date != str(): """ Strict SDP with the package manager that supports it """ @@ -170,7 +171,6 @@ def install_updates_azgps_coordinated(self, maintenance_window, package_manager, remaining_time = maintenance_window.get_remaining_time_in_minutes() try: - # TBD: Pseudo code for strict sdp: this will change and follow install_updates() logic to get_available_updates() and then use get_security_packages to only mark the packages as Security, This is needed in all distros that don't support package classifications all_packages, all_package_versions = package_manager.get_all_updates(cached=False) packages, package_versions = package_manager.get_security_updates() self.last_still_needed_packages = list(all_packages) @@ -192,8 +192,7 @@ def install_updates_azgps_coordinated(self, maintenance_window, package_manager, install_result = Constants.FAILED for i in range(0, Constants.MAX_INSTALLATION_RETRY_COUNT): - # TBD: Do we need batch processing in AzGPS-coordinated? No, as we are only installing security updates without passing in a list of packages. - code, out = package_manager.install_security_updates_azgps_coordinated() # TBD: this will change to pass in the compiled list of packages to update + code, out = package_manager.install_security_updates_azgps_coordinated() installed_update_count += self.perform_status_reconciliation_conditionally(package_manager) remaining_time = maintenance_window.get_remaining_time_in_minutes() diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 6000789d..615ebd5e 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -33,26 +33,15 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.cmd_clean_cache = "sudo tdnf clean expire-cache" self.cmd_repo_refresh = "sudo tdnf -q list updates" - # fetch snapshottime from health_store_id - self.snapshot_posix_time = self.__get_posix_time(execution_config.max_patch_publish_date, env_layer) - # Pseudo code for strict sdp - # if execution_config.operation.lower() == Constants.INSTALLATION.lower() and execution_config.max_patch_publish_date is not None and execution_config.health_store_id is not None: - # verify if VM matches config azlinux version and tdnf version - # if azlinux < 3.0.20241005 and tdnf < 3.5.8-3: - # self.snapshot_posix_time = Compute posixtime from execution_config.max_patch_publish_date // could throw an exception and fail the entire operation any issues with posix time computation - # else: - # Log and return unsupported error: VM config doesn't match the requirements for strict SDP - # else: - # self.snapshot_posix_time = str() # no snapshottime - # Support to get updates and their dependencies - self.tdnf_check = self.__generate_command_with_snapshottime('sudo tdnf -q list updates ', self.snapshot_posix_time) # TBD: snapshottime doesn't work here and only returns the latest version from the timestamp it is valida for. For older snapshottime, it returns empty - self.single_package_check_versions = self.__generate_command_with_snapshottime('sudo tdnf list available ', self.snapshot_posix_time) - self.single_package_check_installed = self.__generate_command_with_snapshottime('sudo tdnf list installed ', self.snapshot_posix_time) # doesn't need snapshottime - self.single_package_upgrade_simulation_cmd = self.__generate_command_with_snapshottime('sudo tdnf install --assumeno --skip-broken ', self.snapshot_posix_time) + self.tdnf_check = 'sudo tdnf -q list updates ' + self.single_package_check_versions = 'sudo tdnf list available ' + self.single_package_check_installed = 'sudo tdnf list installed ' + self.single_package_upgrade_simulation_cmd = 'sudo tdnf install --assumeno --skip-broken ' # Install update - self.single_package_upgrade_cmd = self.__generate_command_with_snapshottime('sudo tdnf -y install --skip-broken ', self.snapshot_posix_time) + self.single_package_upgrade_cmd = 'sudo tdnf -y install --skip-broken ' + self.install_security_updates_azgps_coordinated_cmd = 'sudo tdnf -y upgrade --skip-broken ' # Package manager exit code(s) self.tdnf_exitcode_ok = 0 @@ -87,7 +76,8 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.STR_TOTAL_DOWNLOAD_SIZE = "Total download size: " self.version_comparator = VersionComparator() - # if an Auto Patching request comes in on a Azure Linux machine with Security and/or Critical classifications selected, we need to install all patches, since classifications aren't available in Azure Linux repository + # Todo: Q: Do we need this now that get_security_updates() returns all updates? + # if an Auto Patching request comes in on an Azure Linux machine with Security and/or Critical classifications selected, we need to install all patches, since classifications aren't available in Azure Linux repository installation_included_classifications = [] if execution_config.included_classifications_list is None else execution_config.included_classifications_list if execution_config.health_store_id is not str() and execution_config.operation.lower() == Constants.INSTALLATION.lower() \ and (env_layer.is_distro_azure_linux(str(env_layer.platform.linux_distribution()))) \ @@ -118,24 +108,9 @@ def __get_posix_time(self, datetime_to_convert, env_layer): return posix_time @staticmethod - def __generate_command_with_snapshottime(command_template, snapshotposixtime=str()): + def __generate_command(command_template, snapshotposixtime=str()): # type: (str, str) -> str - # Pseudo code for strict sdp - # generates commands with snapshottime if snapshot_posix_time is not empty and machine meets the requirements of Az Linux and TDNF versions - - """ Prepares a standard command to use snapshottime.""" - # Psuedo code for strict sdp: Oppurtunistic tdnf upgrade to supported version by AzGPS, good to have for later as an enhancement - # finds azlinux major version, and tdnf version - # if azlinux < 3.0.20241005 - # no snaphottime - # if azlinux >= 3.0.20241005 and tdnf < 3.5.8-3 - # 1 attempt to update tdnf - # if succeeds, add snapshottime - # if fails, no snapshottime - # if azlinux >= 3.0.20241005 and tdnf >= 3.5.8-3 - # add snapshottime - if snapshotposixtime == str(): return command_template.replace('', str()) else: @@ -169,25 +144,20 @@ def get_all_updates(self, cached=False): self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(cached), len(self.all_updates_cached))) return self.all_updates_cached, self.all_update_versions_cached # allows for high performance reuse in areas of the code explicitly aware of the cache - out = self.invoke_package_manager(self.tdnf_check) + out = self.invoke_package_manager(self.__generate_command(self.tdnf_check, self.max_patch_publish_date)) self.all_updates_cached, self.all_update_versions_cached = self.extract_packages_and_versions(out) - # Pseudo code for strict sdp: - # if self.snapshot_posix_time != str(): // OR execution_config.max_patch_publish_date is not None: - # run sudo tdnf list available --snapshottime= to get the list of available updates for that snapshottime - # For each package in all_updates_cached: - # Find it's corresponding entry in list available output and fetch the package version from it - self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(False), len(self.all_updates_cached))) return self.all_updates_cached, self.all_update_versions_cached def get_security_updates(self): """Get missing security updates. NOTE: Classification based categorization of patches is not available in Azure Linux as of now""" - self.composite_logger.log_verbose("[TDNF] Discovering 'security' packages...") - security_packages, security_package_versions = [], [] + self.composite_logger.log_verbose("[TDNF] Discovering all packages as 'security' packages, since Azure Linux does not support package classification...") + security_packages, security_package_versions = self.get_all_updates(cached=False) self.composite_logger.log_debug("[TDNF] Discovered 'security' packages. [Count={0}]".format(len(security_packages))) return security_packages, security_package_versions def get_other_updates(self): + # todo: Q: What should this function do in Azure Linux? Since all updates are considered 'security' updates, should this return anything? """Get missing other updates. NOTE: This function will return all available packages since Azure Linux does not support package classification in it's repository""" self.composite_logger.log_verbose("[TDNF] Discovering 'other' packages...") @@ -199,7 +169,10 @@ def get_other_updates(self): return all_packages, all_package_versions def set_max_patch_publish_date(self, max_patch_publish_date=str()): - pass + """Set the max patch publish date in POSIX time for strict SDP""" + self.composite_logger.log_debug("[TDNF] Setting max patch publish date. [MaxPatchPublishDate={0}]".format(str(max_patch_publish_date))) + self.max_patch_publish_date = self.__get_posix_time(max_patch_publish_date, self.env_layer) + self.composite_logger.log_debug("[TDNF] Set max patch publish date. [MaxPatchPublishDatePosixTime={0}]".format(str(self.max_patch_publish_date))) # endregion # region Output Parser(s) @@ -272,7 +245,10 @@ def install_updates_fail_safe(self, excluded_packages): return def install_security_updates_azgps_coordinated(self): - pass + """Install security updates in Azure Linux following strict SDP""" + command = self.__generate_command(self.install_security_updates_azgps_coordinated_cmd, self.max_patch_publish_date) + out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) + return code, out # endregion # region Package Information @@ -382,8 +358,9 @@ def get_dependent_list(self, packages): package_names += ' ' package_names += package - self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(self.single_package_upgrade_simulation_cmd + package_names))) - output = self.invoke_package_manager(self.single_package_upgrade_simulation_cmd + package_names) + cmd = self.single_package_upgrade_simulation_cmd + package_names + self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(cmd))) + output = self.invoke_package_manager(cmd) dependencies = self.extract_dependencies(output, packages) self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Packages={0}][DependencyCount={1}]".format(str(packages), len(dependencies))) return dependencies diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index e25f5e99..8cf2ae1f 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -668,16 +668,16 @@ def test_install_all_packages_for_azure_linux_autopatching(self): self.assertTrue(substatus_file_data[1]["status"].lower() == Constants.STATUS_SUCCESS.lower()) self.assertTrue(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["installedPatchCount"] == 9) self.assertEqual(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][1]["name"], "azurelinux-repos-ms-oss.noarch") - self.assertTrue("Other" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][1]["classifications"])) + self.assertTrue("Security" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][1]["classifications"])) self.assertTrue("Installed" == json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][1]["patchInstallationState"]) self.assertEqual(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][2]["name"], "libseccomp.x86_64") - self.assertTrue("Other" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][2]["classifications"])) + self.assertTrue("Security" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][2]["classifications"])) self.assertTrue("Installed" == json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][2]["patchInstallationState"]) self.assertEqual(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][0]["name"], "azurelinux-release.noarch") - self.assertTrue("Other" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][0]["classifications"])) + self.assertTrue("Security" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][0]["classifications"])) self.assertTrue("Installed" == json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][0]["patchInstallationState"]) self.assertEqual(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][3]["name"], "python3.x86_64") - self.assertTrue("Other" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][3]["classifications"])) + self.assertTrue("Security" in str(json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][3]["classifications"])) self.assertTrue("Installed" == json.loads(substatus_file_data[1]["formattedMessage"]["message"])["patches"][3]["patchInstallationState"]) self.assertTrue(substatus_file_data[2]["name"] == Constants.PATCH_METADATA_FOR_HEALTHSTORE) self.assertTrue(substatus_file_data[2]["status"].lower() == Constants.STATUS_SUCCESS.lower()) diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index f7f53922..a46b0913 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -347,7 +347,7 @@ def test_inclusion_type_all(self): self.assertEqual("6.6.78.1-1.azl3", package_versions[8]) def test_inclusion_type_critical(self): - """Unit test for tdnf package manager with inclusion and Classification = Critical. Returns no packages since classifications are not available in Azure Linux""" + """Unit test for tdnf package manager with inclusion and Classification = Critical. Returns all packages since classifications are not available in Azure Linux, hence everything is considered as Critical.""" self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') self.assertTrue(package_manager is not None) @@ -365,8 +365,8 @@ def test_inclusion_type_critical(self): # test for get_available_updates available_updates, package_versions = package_manager.get_available_updates(package_filter) - self.assertTrue(available_updates == []) - self.assertTrue(package_versions == []) + self.assertEqual(9, len(available_updates)) + self.assertEqual(9, len(package_versions)) def test_inclusion_type_other(self): """Unit test for tdnf package manager with inclusion and Classification = Other. All packages are considered are 'Other' since AzLinux does not have patch classification""" From 528bb89cd9c730ad363f103044f51a60fb6cc6fb Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 10 Jul 2025 06:55:01 -0700 Subject: [PATCH 07/13] AzureLinux Strict SDP support: Addressing PR comments #2 --- src/core/src/bootstrap/EnvLayer.py | 9 ++++++--- .../src/package_managers/TdnfPackageManager.py | 16 +--------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 8bbc4d60..70a44beb 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -404,9 +404,12 @@ def standard_datetime_to_utc(std_datetime): @staticmethod def datetime_string_to_posix_time(datetime_string, format_string): - """ Converts string of given format to posix datetime string. """ + """ Converts string of given format to posix datetime string. + type: (str, str) -> str""" # eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str) - datetime_object = datetime.datetime.strptime(datetime_string, format_string) - posix_timestamp = str(int(time.mktime(datetime_object.timetuple()))) + posix_timestamp = str() + if datetime_string != str(): + datetime_object = datetime.datetime.strptime(datetime_string, format_string) + posix_timestamp = str(int(time.mktime(datetime_object.timetuple()))) return posix_timestamp # endregion - DateTime emulator and extensions diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 615ebd5e..54cf5306 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -93,20 +93,6 @@ def refresh_repo(self): self.invoke_package_manager(self.cmd_repo_refresh) # region Strict SDP using SnapshotTime - def __get_posix_time(self, datetime_to_convert, env_layer): - """Converts date str received to POSIX time string""" - posix_time = str() - datetime_to_convert_format = '%Y%m%dT%H%M%SZ' - self.composite_logger.log_debug("[TDNF] Getting POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}]".format(str(datetime_to_convert), datetime_to_convert_format)) - try: - if datetime_to_convert != str(): - posix_time = env_layer.datetime.datetime_string_to_posix_time(datetime_to_convert, datetime_to_convert_format) - except Exception as error: - self.composite_logger.log_debug("[TDNF] Could not fetch POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}][ComputedPosixTime={2}][Error={3}]".format(str(datetime_to_convert), datetime_to_convert_format, posix_time, repr(error))) - - self.composite_logger.log_debug("[TDNF] Computed POSIX time from given datetime. [DateTimeToConvert={0}][DateTimeStringFormat={1}][ComputedPosixTime={2}]".format(str(datetime_to_convert), datetime_to_convert_format, posix_time)) - return posix_time - @staticmethod def __generate_command(command_template, snapshotposixtime=str()): # type: (str, str) -> str @@ -171,7 +157,7 @@ def get_other_updates(self): def set_max_patch_publish_date(self, max_patch_publish_date=str()): """Set the max patch publish date in POSIX time for strict SDP""" self.composite_logger.log_debug("[TDNF] Setting max patch publish date. [MaxPatchPublishDate={0}]".format(str(max_patch_publish_date))) - self.max_patch_publish_date = self.__get_posix_time(max_patch_publish_date, self.env_layer) + self.max_patch_publish_date = self.env_layer.datetime.datetime_string_to_posix_time(max_patch_publish_date, '%Y%m%dT%H%M%SZ') self.composite_logger.log_debug("[TDNF] Set max patch publish date. [MaxPatchPublishDatePosixTime={0}]".format(str(self.max_patch_publish_date))) # endregion From 0d40748c731b5b8874543166b5605825c8b8a6c6 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Wed, 23 Jul 2025 15:44:15 -0700 Subject: [PATCH 08/13] AzureLinux Strict SDP support: Adding minimum requirements check --- src/core/src/bootstrap/Constants.py | 2 + src/core/src/bootstrap/EnvLayer.py | 22 ++++--- src/core/src/core_logic/PatchInstaller.py | 3 +- .../AptitudePackageManager.py | 3 + .../src/package_managers/PackageManager.py | 4 ++ .../package_managers/TdnfPackageManager.py | 61 ++++++++++++++++++- .../src/package_managers/YumPackageManager.py | 3 + .../package_managers/ZypperPackageManager.py | 3 + src/core/tests/Test_EnvLayer.py | 1 - 9 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 492122c3..8c5d0f0f 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -205,6 +205,8 @@ class StatusTruncationConfig(EnumBackport): YUM = 'yum' ZYPPER = 'zypper' + TDNF_MINIMUM_VERSION_FOR_STRICT_SDP = "3.5.8-3.azl3" # minimum version of tdnf required to support Strict SDP + # Package Statuses INSTALLED = 'Installed' FAILED = 'Failed' diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 70a44beb..5794905d 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -46,6 +46,15 @@ def __init__(self): def is_distro_azure_linux(distro_name): return any(x in distro_name for x in Constants.AZURE_LINUX) + @staticmethod + def is_distro_azure_linux_3_or_beyond(): + # type: () -> bool + """ Checks if the current distro is Azure Linux 3 """ + + version = distro.os_release_attr('version') + major = version.split('.')[0] if version else None + return major is not None and int(major) >= 3 + def get_package_manager(self): # type: () -> str """ Detects package manager type """ @@ -243,10 +252,6 @@ def cpu_arch(): # architecture def vm_name(): # machine name return platform.node() - @staticmethod - def version(): - return str() if (EnvLayer.get_python_major_version() == 2) else distro.os_release_attr('version') - # endregion - Platform extensions # region - File system extensions @@ -405,11 +410,8 @@ def standard_datetime_to_utc(std_datetime): @staticmethod def datetime_string_to_posix_time(datetime_string, format_string): """ Converts string of given format to posix datetime string. - type: (str, str) -> str""" + type: (str, str) -> int""" # eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str) - posix_timestamp = str() - if datetime_string != str(): - datetime_object = datetime.datetime.strptime(datetime_string, format_string) - posix_timestamp = str(int(time.mktime(datetime_object.timetuple()))) - return posix_timestamp + datetime_object = datetime.datetime.strptime(datetime_string, format_string) + return int(time.mktime(datetime_object.timetuple())) # endregion - DateTime emulator and extensions diff --git a/src/core/src/core_logic/PatchInstaller.py b/src/core/src/core_logic/PatchInstaller.py index 6247433f..30da7b35 100644 --- a/src/core/src/core_logic/PatchInstaller.py +++ b/src/core/src/core_logic/PatchInstaller.py @@ -79,9 +79,8 @@ def start_installation(self, simulate=False): if self.execution_config.max_patch_publish_date != str(): self.package_manager.set_max_patch_publish_date(self.execution_config.max_patch_publish_date) - # todo: validate if this meets the Az Linux and tdnf requirements - if self.package_manager.max_patch_publish_date != str(): + if self.package_manager.max_patch_publish_date != str() and self.package_manager.meets_azgps_coordinated_requirements(): """ Strict SDP with the package manager that supports it """ installed_update_count, update_run_successful, maintenance_window_exceeded = self.install_updates_azgps_coordinated(maintenance_window, package_manager, simulate) package_manager.set_package_manager_setting(Constants.PACKAGE_MGR_SETTING_REPEAT_PATCH_OPERATION, bool(not update_run_successful)) diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index a16852a1..c3884258 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -488,6 +488,9 @@ def install_security_updates_azgps_coordinated(self): command = self.__generate_command_with_custom_sources(self.install_security_updates_azgps_coordinated_cmd, source_parts=source_parts, source_list=source_list) out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) return code, out + + def meets_azgps_coordinated_requirements(self): + return True # endregion # region Package Information diff --git a/src/core/src/package_managers/PackageManager.py b/src/core/src/package_managers/PackageManager.py index b0fa6efe..fe44c93e 100644 --- a/src/core/src/package_managers/PackageManager.py +++ b/src/core/src/package_managers/PackageManager.py @@ -346,6 +346,10 @@ def install_update_and_dependencies_and_get_status(self, package_and_dependencie @abstractmethod def install_security_updates_azgps_coordinated(self): pass + + @abstractmethod + def meets_azgps_coordinated_requirements(self): + return # endregion # region Package Information diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 54cf5306..e5c6cb75 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -157,7 +157,7 @@ def get_other_updates(self): def set_max_patch_publish_date(self, max_patch_publish_date=str()): """Set the max patch publish date in POSIX time for strict SDP""" self.composite_logger.log_debug("[TDNF] Setting max patch publish date. [MaxPatchPublishDate={0}]".format(str(max_patch_publish_date))) - self.max_patch_publish_date = self.env_layer.datetime.datetime_string_to_posix_time(max_patch_publish_date, '%Y%m%dT%H%M%SZ') + self.max_patch_publish_date = str(self.env_layer.datetime.datetime_string_to_posix_time(max_patch_publish_date, '%Y%m%dT%H%M%SZ')) if max_patch_publish_date != str() else max_patch_publish_date self.composite_logger.log_debug("[TDNF] Set max patch publish date. [MaxPatchPublishDatePosixTime={0}]".format(str(self.max_patch_publish_date))) # endregion @@ -235,6 +235,65 @@ def install_security_updates_azgps_coordinated(self): command = self.__generate_command(self.install_security_updates_azgps_coordinated_cmd, self.max_patch_publish_date) out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) return code, out + + def meets_azgps_coordinated_requirements(self): + """ Check if the system meets the requirements for Azure Linux strict safe deployment and attempt to update TDNF if necessary """ + self.composite_logger.log_debug("[TDNF] Checking if system meets Azure Linux security updates requirements...") + # Check if the system is Azure Linux 3.0 or beyond + if not self.env_layer.is_distro_azure_linux_3_or_beyond(): + self.composite_logger.log_error("[TDNF] The system does not meet minimum Azure Linux requirement of 3.0 or above for strict safe deployment. Defaulting to regular upgrades.") + self.set_max_patch_publish_date() # fall-back + return False + else: + if self.is_miminum_tdnf_version_for_strict_sdp_installed(): + self.composite_logger.log_debug("[TDNF] Minimum tdnf version for strict safe deployment is installed.") + return True + else: + if not self.attempt_tdnf_update_to_meet_strict_sdp_requirements(): + error_msg = "Failed to meet minimum TDNF version requirement for strict safe deployment. Defaulting to regular upgrades." + self.composite_logger.log_error(error_msg + "[Error={0}]".format(repr(error_msg))) + self.status_handler.add_error_to_status(error_msg) + self.set_max_patch_publish_date() # fall-back + return False + return True + + def is_miminum_tdnf_version_for_strict_sdp_installed(self): + """Check if at least the minimum required version of TDNF is installed""" + self.composite_logger.log_debug("[TDNF] Checking if minimum TDNF version required for strict safe deployment is installed...") + tdnf_version = self.get_tdnf_version() + if tdnf_version is None: + self.composite_logger.log_error("[TDNF] Failed to get TDNF version. Cannot proceed with strict safe deployment. Defaulting to regular upgrades.") + return False + elif not self.version_comparator.compare_versions(tdnf_version, Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP) >= 0: + self.composite_logger.log_warning("[TDNF] TDNF version installed is less than the minimum required version. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP)) + return False + return True + + def get_tdnf_version(self): + """Get the version of TDNF installed on the system""" + self.composite_logger.log_debug("[TDNF] Getting tdnf version...") + cmd = "rpm -q tdnf | sed -E 's/tdnf-([0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+).*/\\1/'" + code, output = self.env_layer.run_command_output(cmd, False, False) + if code == 0: + # Sample output: 3.5.8-3 + version = output.split()[0] if output else None + self.composite_logger.log_debug("[TDNF] Installed TDNF version [Version={0}]".format(version)) + return version + else: + self.composite_logger.log_error("[TDNF] Failed to get TDNF version. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output)) + return None + + def attempt_tdnf_update_to_meet_strict_sdp_requirements(self): + """Attempt to update TDNF to meet the minimum version required for strict SDP""" + self.composite_logger.log_debug("[TDNF] Attempting to update TDNF to meet strict safe deployment requirements...") + cmd = "sudo tdnf -y install tdnf-" + Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP + code, output = self.env_layer.run_command_output(cmd, no_output=True, chk_err=False) + if code == 0: + self.composite_logger.log_debug("[TDNF] Successfully updated TDNF. [Command={0}][Code={1}]".format(cmd, code)) + return True + else: + self.composite_logger.log_error("[TDNF] Failed to update TDNF. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output)) + return False # endregion # region Package Information diff --git a/src/core/src/package_managers/YumPackageManager.py b/src/core/src/package_managers/YumPackageManager.py index dc888dd8..1608b41a 100644 --- a/src/core/src/package_managers/YumPackageManager.py +++ b/src/core/src/package_managers/YumPackageManager.py @@ -266,6 +266,9 @@ def install_updates_fail_safe(self, excluded_packages): def install_security_updates_azgps_coordinated(self): pass + + def meets_azgps_coordinated_requirements(self): + return # endregion # region Package Information diff --git a/src/core/src/package_managers/ZypperPackageManager.py b/src/core/src/package_managers/ZypperPackageManager.py index 6fe5c4d0..c1635f08 100644 --- a/src/core/src/package_managers/ZypperPackageManager.py +++ b/src/core/src/package_managers/ZypperPackageManager.py @@ -420,6 +420,9 @@ def install_updates_fail_safe(self, excluded_packages): def install_security_updates_azgps_coordinated(self): pass + + def meets_azgps_coordinated_requirements(self): + return # endregion # region Package Information diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index e9ccb598..24bf5c73 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -108,7 +108,6 @@ def test_platform(self): self.envlayer.platform.os_type() self.envlayer.platform.cpu_arch() self.envlayer.platform.vm_name() - self.envlayer.platform.version() if __name__ == '__main__': From c078f902b143b56aae0eb0873458b6e89578acb2 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Wed, 23 Jul 2025 16:16:07 -0700 Subject: [PATCH 09/13] AzureLinux Strict SDP support: Addressing PR comments #3 --- .../package_managers/TdnfPackageManager.py | 16 +++++++------ src/core/tests/Test_TdnfPackageManager.py | 24 +++---------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index e5c6cb75..34a869cf 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -76,7 +76,6 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.STR_TOTAL_DOWNLOAD_SIZE = "Total download size: " self.version_comparator = VersionComparator() - # Todo: Q: Do we need this now that get_security_updates() returns all updates? # if an Auto Patching request comes in on an Azure Linux machine with Security and/or Critical classifications selected, we need to install all patches, since classifications aren't available in Azure Linux repository installation_included_classifications = [] if execution_config.included_classifications_list is None else execution_config.included_classifications_list if execution_config.health_store_id is not str() and execution_config.operation.lower() == Constants.INSTALLATION.lower() \ @@ -136,23 +135,26 @@ def get_all_updates(self, cached=False): return self.all_updates_cached, self.all_update_versions_cached def get_security_updates(self): - """Get missing security updates. NOTE: Classification based categorization of patches is not available in Azure Linux as of now""" - self.composite_logger.log_verbose("[TDNF] Discovering all packages as 'security' packages, since Azure Linux does not support package classification...") + """Get missing security updates. NOTE: Classification based categorization of patches is not available in TDNF as of now""" + self.composite_logger.log_verbose("[TDNF] Discovering all packages as 'security' packages, since TDNF does not support package classification...") security_packages, security_package_versions = self.get_all_updates(cached=False) self.composite_logger.log_debug("[TDNF] Discovered 'security' packages. [Count={0}]".format(len(security_packages))) return security_packages, security_package_versions def get_other_updates(self): - # todo: Q: What should this function do in Azure Linux? Since all updates are considered 'security' updates, should this return anything? - """Get missing other updates. - NOTE: This function will return all available packages since Azure Linux does not support package classification in it's repository""" + """Get missing other updates.""" self.composite_logger.log_verbose("[TDNF] Discovering 'other' packages...") other_packages, other_package_versions = [], [] all_packages, all_package_versions = self.get_all_updates(True) + security_packages, security_package_versions = self.get_security_updates() + for index, package in enumerate(all_packages): + if package not in security_packages: + other_packages.append(package) + other_package_versions.append(all_package_versions[index]) self.composite_logger.log_debug("[TDNF] Discovered 'other' packages. [Count={0}]".format(len(other_packages))) - return all_packages, all_package_versions + return other_packages, other_package_versions def set_max_patch_publish_date(self, max_patch_publish_date=str()): """Set the max patch publish date in POSIX time for strict SDP""" diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index a46b0913..c1dd8748 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -369,7 +369,7 @@ def test_inclusion_type_critical(self): self.assertEqual(9, len(package_versions)) def test_inclusion_type_other(self): - """Unit test for tdnf package manager with inclusion and Classification = Other. All packages are considered are 'Other' since AzLinux does not have patch classification""" + """Unit test for tdnf package manager with inclusion and Classification = Other. All packages are considered are 'Security' since TDNF does not have patch classification""" self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') self.assertTrue(package_manager is not None) @@ -389,26 +389,8 @@ def test_inclusion_type_other(self): available_updates, package_versions = package_manager.get_available_updates(package_filter) self.assertTrue(available_updates is not None) self.assertTrue(package_versions is not None) - self.assertEqual(9, len(available_updates)) - self.assertEqual(9, len(package_versions)) - self.assertEqual("azurelinux-release.noarch", available_updates[0]) - self.assertEqual("3.0-16.azl3", package_versions[0]) - self.assertEqual("azurelinux-repos-ms-oss.noarch", available_updates[1]) - self.assertEqual("3.0-3.azl3", package_versions[1]) - self.assertEqual("libseccomp.x86_64", available_updates[2]) - self.assertEqual("2.5.4-1.azl3", package_versions[2]) - self.assertEqual("python3.x86_64", available_updates[3]) - self.assertEqual("3.12.3-6.azl3", package_versions[3]) - self.assertEqual("libxml2.x86_64", available_updates[4]) - self.assertEqual("2.11.5-1.azl3", package_versions[4]) - self.assertEqual("dracut.x86_64", available_updates[5]) - self.assertEqual("102-7.azl3", package_versions[5]) - self.assertEqual("hyperv-daemons-license.noarch", available_updates[6]) - self.assertEqual("6.6.78.1-1.azl3", package_versions[6]) - self.assertEqual("hypervvssd.x86_64", available_updates[7]) - self.assertEqual("6.6.78.1-1.azl3", package_versions[7]) - self.assertEqual("hypervkvpd.x86_64", available_updates[8]) - self.assertEqual("6.6.78.1-1.azl3", package_versions[8]) + self.assertEqual(0, len(available_updates)) + self.assertEqual(0, len(package_versions)) def test_inclusion_only(self): """Unit test for tdnf package manager with inclusion only and NotSelected Classifications""" From b0de63b88fbe4663cacc2e68909530462ab2cafd Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 24 Jul 2025 13:12:49 -0700 Subject: [PATCH 10/13] AzureLinux Strict SDP support: Adding UTs --- .../package_managers/TdnfPackageManager.py | 8 +- src/core/tests/Test_CoreMain.py | 47 ++++++ src/core/tests/Test_EnvLayer.py | 27 ++++ src/core/tests/Test_TdnfPackageManager.py | 140 ++++++++++++++++++ .../tests/library/LegacyEnvLayerExtensions.py | 18 +++ 5 files changed, 236 insertions(+), 4 deletions(-) diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 34a869cf..14c3e276 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -95,7 +95,6 @@ def refresh_repo(self): @staticmethod def __generate_command(command_template, snapshotposixtime=str()): # type: (str, str) -> str - if snapshotposixtime == str(): return command_template.replace('', str()) else: @@ -247,7 +246,7 @@ def meets_azgps_coordinated_requirements(self): self.set_max_patch_publish_date() # fall-back return False else: - if self.is_miminum_tdnf_version_for_strict_sdp_installed(): + if self.is_minimum_tdnf_version_for_strict_sdp_installed(): self.composite_logger.log_debug("[TDNF] Minimum tdnf version for strict safe deployment is installed.") return True else: @@ -259,14 +258,15 @@ def meets_azgps_coordinated_requirements(self): return False return True - def is_miminum_tdnf_version_for_strict_sdp_installed(self): + def is_minimum_tdnf_version_for_strict_sdp_installed(self): """Check if at least the minimum required version of TDNF is installed""" self.composite_logger.log_debug("[TDNF] Checking if minimum TDNF version required for strict safe deployment is installed...") tdnf_version = self.get_tdnf_version() + minimum_tdnf_version_for_strict_sdp = re.match(r"(\d+\.\d+\.\d+-\d+)", Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP).group(1) if tdnf_version is None: self.composite_logger.log_error("[TDNF] Failed to get TDNF version. Cannot proceed with strict safe deployment. Defaulting to regular upgrades.") return False - elif not self.version_comparator.compare_versions(tdnf_version, Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP) >= 0: + elif not self.version_comparator.compare_versions(tdnf_version, minimum_tdnf_version_for_strict_sdp) >= 0: self.composite_logger.log_warning("[TDNF] TDNF version installed is less than the minimum required version. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP)) return False return True diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 8cf2ae1f..4569a4f6 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -27,6 +27,7 @@ from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions from core.tests.library.RuntimeCompositor import RuntimeCompositor +from core.src.external_dependencies import distro class TestCoreMain(unittest.TestCase): @@ -56,6 +57,9 @@ def mock_os_remove(self, file_to_remove): def mock_os_path_exists(self, patch_to_validate): return False + def mock_distro_os_release_attr_return_azure_linux_3(self, attribute): + return '3.0.0' + def test_operation_fail_for_non_autopatching_request(self): # Test for non auto patching request argument_composer = ArgumentComposer() @@ -690,6 +694,49 @@ def test_install_all_packages_for_azure_linux_autopatching(self): LegacyEnvLayerExtensions.LegacyPlatform.linux_distribution = backup_envlayer_platform_linux_distribution + def test_install_all_packages_for_azure_linux_strict_sdp(self): + # backups + backup_envlayer_platform_linux_distribution = LegacyEnvLayerExtensions.LegacyPlatform.linux_distribution + backup_distro_os_release_attr = distro.os_release_attr + + LegacyEnvLayerExtensions.LegacyPlatform.linux_distribution = self.mock_linux_distribution_to_return_azure_linux + distro.os_release_attr = self.mock_distro_os_release_attr_return_azure_linux_3 + + argument_composer = ArgumentComposer() + classifications_to_include = ["Security", "Critical"] + argument_composer.maximum_duration = "PT3H" + argument_composer.classifications_to_include = classifications_to_include + argument_composer.patches_to_include = ["MaxPatchPublishDate=20250210T000000Z", "AzGPS_Mitigation_Mode_No_SLA"] + argument_composer.reboot_setting = 'Always' + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.TDNF) + runtime.set_legacy_test_type("HappyPath") + CoreMain(argument_composer.get_composed_arguments()) + + self.assertEqual(runtime.package_manager.max_patch_publish_date, "1739174400") + + # check telemetry events + self.__check_telemetry_events(runtime) + + # check status file + with runtime.env_layer.file_system.open(runtime.execution_config.status_file_path, 'r') as file_handle: + substatus_file_data = json.load(file_handle)[0]["status"]["substatus"] + self.assertEqual(len(substatus_file_data), 4) + self.assertTrue(substatus_file_data[0]["name"] == Constants.PATCH_ASSESSMENT_SUMMARY) + self.assertTrue(substatus_file_data[0]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + self.assertTrue(substatus_file_data[1]["name"] == Constants.PATCH_INSTALLATION_SUMMARY) + self.assertTrue(substatus_file_data[1]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + self.assertTrue(substatus_file_data[2]["name"] == Constants.PATCH_METADATA_FOR_HEALTHSTORE) + self.assertTrue(substatus_file_data[2]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + substatus_file_data_patch_metadata_summary = json.loads(substatus_file_data[2]["formattedMessage"]["message"]) + self.assertEqual(substatus_file_data_patch_metadata_summary["patchVersion"], "") + self.assertTrue(substatus_file_data_patch_metadata_summary["shouldReportToHealthStore"]) + self.assertTrue(substatus_file_data[3]["name"] == Constants.CONFIGURE_PATCHING_SUMMARY) + self.assertTrue(substatus_file_data[3]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + runtime.stop() + + LegacyEnvLayerExtensions.LegacyPlatform.linux_distribution = backup_envlayer_platform_linux_distribution + distro.os_release_attr = backup_distro_os_release_attr + # test with both assessment mode and patch mode set in configure patching or install patches or assess patches or auto assessment def test_auto_assessment_success_with_configure_patching_in_prev_operation_on_same_sequence(self): """Unit test for auto assessment request with configure patching completed on the sequence before. Result: should retain prev substatus and update only PatchAssessmentSummary""" diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 24bf5c73..3662951e 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -17,6 +17,7 @@ import unittest from core.src.bootstrap.EnvLayer import EnvLayer from core.src.bootstrap.Constants import Constants +from core.src.external_dependencies import distro class TestExecutionConfig(unittest.TestCase): @@ -61,6 +62,15 @@ def mock_run_command_for_tdnf(self, cmd, no_output=False, chk_err=False): if cmd.find("which tdnf") > -1: return 0, '' return -1, '' + + def mock_distro_os_release_attr_return_azure_linux_3(self, attribute): + return '3.0.0' + + def mock_distro_os_release_attr_return_azure_linux_2(self, attribute): + return '2.9.0' + + def mock_distro_os_release_attr_return_none(self, attribute): + return None # endregion def test_get_package_manager(self): @@ -95,6 +105,23 @@ def test_get_package_manager(self): self.envlayer.platform.linux_distribution = self.backup_linux_distribution platform.system = self.backup_platform_system + def test_is_distro_azure_linux_3_or_beyond(self): + self.backup_envlayer_distro_os_release_attr = distro.os_release_attr + + test_input_output_table = [ + [self.mock_distro_os_release_attr_return_azure_linux_3, True], + [self.mock_distro_os_release_attr_return_azure_linux_2, False], + [self.mock_distro_os_release_attr_return_none, False] + ] + + for row in test_input_output_table: + distro.os_release_attr = row[0] + result = self.envlayer.is_distro_azure_linux_3_or_beyond() + self.assertEqual(result, row[1]) + + # restore original methods + distro.os_release_attr = self.backup_envlayer_distro_os_release_attr + def test_filesystem(self): # only validates if these invocable without exceptions backup_retry_count = Constants.MAX_FILE_OPERATION_RETRY_COUNT diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index c1dd8748..b2526f58 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -27,6 +27,7 @@ from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.RuntimeCompositor import RuntimeCompositor +from core.src.external_dependencies import distro class TestTdnfPackageManager(unittest.TestCase): @@ -46,6 +47,38 @@ def mock_linux_distribution_to_return_azure_linux(self): def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode='a+'): raise Exception + + def mock_run_command_output_return_tdnf_3(self, cmd, no_output=False, chk_err=True): + """ Mock for run_command_output to return tdnf 3 """ + return 0, "3.5.8-3\n" + + def mock_run_command_output_return_1(self, cmd, no_output=False, chk_err=True): + """ Mock for run_command_output to return None """ + return 1, "No output available\n" + + def mock_run_command_output_return_0(self, cmd, no_output=False, chk_err=True): + return 0, "Successfully executed command\n" + + def mock_get_tdnf_version_return_tdnf_3_5_8_3(self): + return "3.5.8-3" + + def mock_get_tdnf_version_return_tdnf_4_0(self): + return "4.0.0-1" + + def mock_get_tdnf_version_return_tdnf_2_5(self): + return "2.5.0-1" + + def mock_get_tdnf_version_return_tdnf_3_5_8_2(self): + return "3.5.8-2.azl3" + + def mock_get_tdnf_version_return_None(self): + return None + + def mock_distro_os_release_attr_return_azure_linux_3(self, attribute): + return '3.0.0' + + def mock_distro_os_release_attr_return_azure_linux_2(self, attribute): + return '2.9.0' # endregion # region Utility Functions @@ -853,6 +886,113 @@ def test_revert_auto_os_update_to_system_default(self): self.__assert_std_io(captured_output=captured_output, expected_output=testcase["stdio"]["expected_output"]) self.__assert_reverted_automatic_patch_configuration_settings(package_manager, config_exists=bool(testcase["assertions"]["config_exists"]), config_value_expected=testcase["assertions"]["config_value_expected"]) + def test_set_max_patch_publish_date(self): + """Unit test for tdnf package manager set_max_patch_publish_date method""" + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager is not None) + + input_output_table_for_successful_cases = [ + ["20240702T000000Z", "1719903600"], + ["", ""] + ] + for row in input_output_table_for_successful_cases: + package_manager.set_max_patch_publish_date(row[0]) + self.assertEqual(package_manager.max_patch_publish_date, row[1]) + + # posix time computation throws an exception if the date is not in the correct format + self.assertRaises(ValueError, package_manager.set_max_patch_publish_date, "2024-07-02T00:00:00Z") + + def test_get_tdnf_version(self): + """Unit test for tdnf package manager get_tdnf_version method""" + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager is not None) + self.backup_run_command_output = self.runtime.env_layer.run_command_output + + test_input_output_table = [ + [self.mock_run_command_output_return_tdnf_3, "3.5.8-3"], + [self.mock_run_command_output_return_1, None], + ] + + for row in test_input_output_table: + self.runtime.env_layer.run_command_output = row[0] + version = package_manager.get_tdnf_version() + self.assertEqual(version, row[1]) + + self.runtime.env_layer.run_command_output = self.backup_run_command_output + + def test_is_mininum_tdnf_version_for_strict_sdp_installed(self): + """Unit test for tdnf package manager is_minimum_tdnf_version method""" + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager is not None) + + self.backup_get_tdnf_version = package_manager.get_tdnf_version + + test_input_output_table = [ + [self.mock_get_tdnf_version_return_tdnf_2_5, False], + [self.mock_get_tdnf_version_return_tdnf_3_5_8_2, False], + [self.mock_get_tdnf_version_return_tdnf_3_5_8_3, True], + [self.mock_get_tdnf_version_return_tdnf_4_0, True], + [self.mock_get_tdnf_version_return_None, False] + ] + + for row in test_input_output_table: + package_manager.get_tdnf_version = row[0] + result = package_manager.is_minimum_tdnf_version_for_strict_sdp_installed() + self.assertEqual(result, row[1]) + + package_manager.get_tdnf_version = self.backup_get_tdnf_version + + def test_attempt_tdnf_update_to_meet_strict_sdp_requirements(self): + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager is not None) + + self.backup_run_command_output = self.runtime.env_layer.run_command_output + + input_output_table = [ + [self.mock_run_command_output_return_0, True], + [self.mock_run_command_output_return_1, False], + ] + + for row in input_output_table: + self.runtime.env_layer.run_command_output = row[0] + result = package_manager.attempt_tdnf_update_to_meet_strict_sdp_requirements() + self.assertEqual(result, row[1]) + + self.runtime.env_layer.run_command_output = self.backup_run_command_output + + def test_meets_azgps_coordinated_requirements(self): + package_manager = self.container.get('package_manager') + self.assertTrue(package_manager is not None) + + # backup methods + self.backup_distro_os_release_attr = distro.os_release_attr + self.backup_get_tdnf_version = package_manager.get_tdnf_version + self.backup_run_command_output = self.runtime.env_layer.run_command_output + + """ test cases: + 1. Azure Linux 3 with tdnf version > 3.5.8-3 + 2. Azure Linux 3 with tdnf version = 3.5.8-3 + 3. Azure Linux 3 with tdnf version < 3.5.8-3, will be updated to 3.5.8-3 successfully + 4. Azure Linux 3 with tdnf version < 3.5.8-3, will not be updated to 3.5.8-3 + 5. Azure Linux 2""" + test_input_output_table = [ + [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_4_0, self.backup_run_command_output, True], + [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_3_5_8_3, self.backup_run_command_output, True], + [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_0, True], + [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_1, False], + [self.mock_distro_os_release_attr_return_azure_linux_2, self.backup_distro_os_release_attr, self.backup_run_command_output, False] + ] + + for row in test_input_output_table: + # set test case values + distro.os_release_attr = row[0] + package_manager.get_tdnf_version = row[1] + self.runtime.env_layer.run_command_output = row[2] + + # run test case + result = package_manager.meets_azgps_coordinated_requirements() + self.assertEqual(result, row[3]) + if __name__ == '__main__': unittest.main() diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index aebad18c..dd46dd7f 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -626,6 +626,24 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): elif cmd.find("systemctl disable ") > -1: code = 0 output = 'Auto update service disabled' + elif cmd.find("rpm -q tdnf") > -1: + code = 0 + output = '3.5.8-3' + elif cmd.find('tdnf -y upgrade') > -1: + code = 0 + output = "Loaded plugin: tdnfrepogpgcheck\n" + \ + "Upgrading:\n" + \ + "azurelinux-release noarch 3.0-16.azl3 azurelinux-official-base 847.91k 403.29k\n" + \ + "azurelinux-repos-ms-oss noarch 3.0-3.azl3 azurelinux-official-base 382.51k 258.06k\n\n" + \ + "libseccomp x86_64 2.5.4-1.azl3 azurelinux-official-base 847.91k 403.29k\n" + \ + "python3 x86_64 3.12.3-6.azl3 azurelinux-official-base 382.51k 258.06k\n\n" + \ + "libxml2 x86_64 2.11.5-1.azl3 azurelinux-official-base 847.91k 403.29k\n" + \ + "dracut x86_64 102-7.azl3 azurelinux-official-base 382.51k 258.06k\n\n" + \ + "hyperv-daemons-license noarch 6.6.78.1-1.azl3 azurelinux-official-base 847.91k 403.29k\n" + \ + "hypervvssd x86_64 6.6.78.1-1.azl3 azurelinux-official-base 382.51k 258.06k\n\n" + \ + "hypervkvpd x86_64 6.6.78.1-1.azl3 azurelinux-official-base 847.91k 403.29k\n" + \ + "Total installed size: 1.20M\n" + \ + "Total download size: 661.34k\n" elif self.legacy_test_type == 'SadPath': if cmd.find("cat /proc/cpuinfo | grep name") > -1: code = 0 From 813350e940e303bd318bf2e9416b383b98b772af Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 24 Jul 2025 14:01:51 -0700 Subject: [PATCH 11/13] AzureLinux Strict SDP support: Fixing posix time computation to always consider UTC time as base for calculation --- src/core/src/bootstrap/EnvLayer.py | 11 +++++++++-- src/core/tests/Test_CoreMain.py | 2 +- src/core/tests/Test_TdnfPackageManager.py | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 5794905d..91007099 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -15,6 +15,7 @@ # Requires Python 2.7+ from __future__ import print_function + import datetime import glob import os @@ -412,6 +413,12 @@ def datetime_string_to_posix_time(datetime_string, format_string): """ Converts string of given format to posix datetime string. type: (str, str) -> int""" # eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str) - datetime_object = datetime.datetime.strptime(datetime_string, format_string) - return int(time.mktime(datetime_object.timetuple())) + + # Parse the datetime string + dt = datetime.datetime.strptime(datetime_string, format_string) + + # Convert to POSIX time assuming UTC + epoch = datetime.datetime(1970, 1, 1) + return int((dt - epoch).total_seconds()) + # endregion - DateTime emulator and extensions diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 4569a4f6..a8e9f0ba 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -712,7 +712,7 @@ def test_install_all_packages_for_azure_linux_strict_sdp(self): runtime.set_legacy_test_type("HappyPath") CoreMain(argument_composer.get_composed_arguments()) - self.assertEqual(runtime.package_manager.max_patch_publish_date, "1739174400") + self.assertEqual(runtime.package_manager.max_patch_publish_date, "1739145600") # check telemetry events self.__check_telemetry_events(runtime) diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index b2526f58..509df5de 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -892,7 +892,7 @@ def test_set_max_patch_publish_date(self): self.assertTrue(package_manager is not None) input_output_table_for_successful_cases = [ - ["20240702T000000Z", "1719903600"], + ["20240702T000000Z", "1719878400"], ["", ""] ] for row in input_output_table_for_successful_cases: From 103f34b4566f2dc29f28b015c1c904862a5455b3 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 31 Jul 2025 07:17:17 -0700 Subject: [PATCH 12/13] AzureLinux Strict SDP support: Addressing PR comments #4 --- src/core/src/bootstrap/Constants.py | 4 +- src/core/src/bootstrap/EnvLayer.py | 16 ++--- .../AptitudePackageManager.py | 1 + .../src/package_managers/PackageManager.py | 4 +- .../package_managers/TdnfPackageManager.py | 58 ++++++++++--------- .../src/package_managers/YumPackageManager.py | 3 +- .../package_managers/ZypperPackageManager.py | 3 +- src/core/tests/Test_EnvLayer.py | 13 +++-- src/core/tests/Test_TdnfPackageManager.py | 47 ++++++++++----- .../tests/library/LegacyEnvLayerExtensions.py | 2 +- 10 files changed, 89 insertions(+), 62 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 8c5d0f0f..84598e22 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -205,8 +205,6 @@ class StatusTruncationConfig(EnumBackport): YUM = 'yum' ZYPPER = 'zypper' - TDNF_MINIMUM_VERSION_FOR_STRICT_SDP = "3.5.8-3.azl3" # minimum version of tdnf required to support Strict SDP - # Package Statuses INSTALLED = 'Installed' FAILED = 'Failed' @@ -349,7 +347,7 @@ class TelemetryTaskName(EnumBackport): TELEMETRY_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Azure Linux Agent version. To resolve: http://aka.ms/UpdateLinuxAgent" TELEMETRY_COMPATIBLE_MSG = "Minimum Azure Linux Agent version prerequisite met" PYTHON_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Python version. Minimum Python version required is 2.7. [DetectedPythonVersion={0}]" - INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" # TBD: the link is specific to Canonical, we need a generic one or one link of each distro + INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" # aka.ms link to be distro-agnostic UTC_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # EnvLayer Constants diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index 91007099..f79f7b2c 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -47,14 +47,14 @@ def __init__(self): def is_distro_azure_linux(distro_name): return any(x in distro_name for x in Constants.AZURE_LINUX) - @staticmethod - def is_distro_azure_linux_3_or_beyond(): + def is_distro_azure_linux_3_or_beyond(self): # type: () -> bool """ Checks if the current distro is Azure Linux 3 """ - - version = distro.os_release_attr('version') - major = version.split('.')[0] if version else None - return major is not None and int(major) >= 3 + if self.is_distro_azure_linux(self.platform.linux_distribution()): + version = distro.os_release_attr('version') + major = version.split('.')[0] if version else None + return major is not None and int(major) >= 3 + return False def get_package_manager(self): # type: () -> str @@ -410,8 +410,8 @@ def standard_datetime_to_utc(std_datetime): @staticmethod def datetime_string_to_posix_time(datetime_string, format_string): - """ Converts string of given format to posix datetime string. - type: (str, str) -> int""" + # type: (str, str) -> int + """ Converts string of given format to posix datetime string. """ # eg: Input: datetime_string: 20241220T000000Z (str), format_string: '%Y%m%dT%H%M%SZ' -> Output: 1734681600 (str) # Parse the datetime string diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index c3884258..4ae5d90c 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -490,6 +490,7 @@ def install_security_updates_azgps_coordinated(self): return code, out def meets_azgps_coordinated_requirements(self): + # type: () -> bool return True # endregion diff --git a/src/core/src/package_managers/PackageManager.py b/src/core/src/package_managers/PackageManager.py index fe44c93e..c6ae472a 100644 --- a/src/core/src/package_managers/PackageManager.py +++ b/src/core/src/package_managers/PackageManager.py @@ -349,7 +349,9 @@ def install_security_updates_azgps_coordinated(self): @abstractmethod def meets_azgps_coordinated_requirements(self): - return + # type: () -> bool + """ Returns true if the package manager meets the requirements for azgps coordinated security updates """ + return False # endregion # region Package Information diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 14c3e276..8ea98bb2 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -71,6 +71,9 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ # commands for DNF Automatic updates service self.__init_constants_for_dnf_automatic() + # AzLinux3 Package Manager. + self.azl3_tdnf_packagemanager = self.AzL3TdnfPackageManager() + # Miscellaneous self.set_package_manager_setting(Constants.PKG_MGR_SETTING_IDENTITY, Constants.TDNF) self.STR_TOTAL_DOWNLOAD_SIZE = "Total download size: " @@ -93,7 +96,7 @@ def refresh_repo(self): # region Strict SDP using SnapshotTime @staticmethod - def __generate_command(command_template, snapshotposixtime=str()): + def __generate_command_with_snapshotposixtime_if_specified(command_template, snapshotposixtime=str()): # type: (str, str) -> str if snapshotposixtime == str(): return command_template.replace('', str()) @@ -128,7 +131,7 @@ def get_all_updates(self, cached=False): self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(cached), len(self.all_updates_cached))) return self.all_updates_cached, self.all_update_versions_cached # allows for high performance reuse in areas of the code explicitly aware of the cache - out = self.invoke_package_manager(self.__generate_command(self.tdnf_check, self.max_patch_publish_date)) + out = self.invoke_package_manager(self.__generate_command_with_snapshotposixtime_if_specified(self.tdnf_check, self.max_patch_publish_date)) self.all_updates_cached, self.all_update_versions_cached = self.extract_packages_and_versions(out) self.composite_logger.log_debug("[TDNF] Get all updates : [Cached={0}][PackagesCount={1}]]".format(str(False), len(self.all_updates_cached))) return self.all_updates_cached, self.all_update_versions_cached @@ -143,17 +146,7 @@ def get_security_updates(self): def get_other_updates(self): """Get missing other updates.""" self.composite_logger.log_verbose("[TDNF] Discovering 'other' packages...") - other_packages, other_package_versions = [], [] - - all_packages, all_package_versions = self.get_all_updates(True) - security_packages, security_package_versions = self.get_security_updates() - for index, package in enumerate(all_packages): - if package not in security_packages: - other_packages.append(package) - other_package_versions.append(all_package_versions[index]) - - self.composite_logger.log_debug("[TDNF] Discovered 'other' packages. [Count={0}]".format(len(other_packages))) - return other_packages, other_package_versions + return [], [] def set_max_patch_publish_date(self, max_patch_publish_date=str()): """Set the max patch publish date in POSIX time for strict SDP""" @@ -233,11 +226,12 @@ def install_updates_fail_safe(self, excluded_packages): def install_security_updates_azgps_coordinated(self): """Install security updates in Azure Linux following strict SDP""" - command = self.__generate_command(self.install_security_updates_azgps_coordinated_cmd, self.max_patch_publish_date) + command = self.__generate_command_with_snapshotposixtime_if_specified(self.install_security_updates_azgps_coordinated_cmd, self.max_patch_publish_date) out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) return code, out def meets_azgps_coordinated_requirements(self): + # type: () -> bool """ Check if the system meets the requirements for Azure Linux strict safe deployment and attempt to update TDNF if necessary """ self.composite_logger.log_debug("[TDNF] Checking if system meets Azure Linux security updates requirements...") # Check if the system is Azure Linux 3.0 or beyond @@ -250,7 +244,7 @@ def meets_azgps_coordinated_requirements(self): self.composite_logger.log_debug("[TDNF] Minimum tdnf version for strict safe deployment is installed.") return True else: - if not self.attempt_tdnf_update_to_meet_strict_sdp_requirements(): + if not self.try_tdnf_update_to_meet_strict_sdp_requirements(): error_msg = "Failed to meet minimum TDNF version requirement for strict safe deployment. Defaulting to regular upgrades." self.composite_logger.log_error(error_msg + "[Error={0}]".format(repr(error_msg))) self.status_handler.add_error_to_status(error_msg) @@ -259,42 +253,49 @@ def meets_azgps_coordinated_requirements(self): return True def is_minimum_tdnf_version_for_strict_sdp_installed(self): + # type: () -> bool """Check if at least the minimum required version of TDNF is installed""" self.composite_logger.log_debug("[TDNF] Checking if minimum TDNF version required for strict safe deployment is installed...") tdnf_version = self.get_tdnf_version() - minimum_tdnf_version_for_strict_sdp = re.match(r"(\d+\.\d+\.\d+-\d+)", Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP).group(1) + minimum_tdnf_version_for_strict_sdp = self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP + distro_from_minimum_tdnf_version_for_strict_sdp = re.match(r".*-\d+\.([a-zA-Z0-9]+)$", minimum_tdnf_version_for_strict_sdp).group(1) if tdnf_version is None: self.composite_logger.log_error("[TDNF] Failed to get TDNF version. Cannot proceed with strict safe deployment. Defaulting to regular upgrades.") return False + elif re.match(r".*-\d+\.([a-zA-Z0-9]+)$", tdnf_version).group(1) != distro_from_minimum_tdnf_version_for_strict_sdp: + self.composite_logger.log_warning("[TDNF] TDNF version installed is not from the same Azure Linux distribution as the minimum required version for strict SDP. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP)) + return False elif not self.version_comparator.compare_versions(tdnf_version, minimum_tdnf_version_for_strict_sdp) >= 0: - self.composite_logger.log_warning("[TDNF] TDNF version installed is less than the minimum required version. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP)) + self.composite_logger.log_warning("[TDNF] TDNF version installed is less than the minimum required version for strict SDP. [InstalledVersion={0}][MinimumRequiredVersion={1}]".format(tdnf_version, self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP)) return False return True def get_tdnf_version(self): + # type: () -> any """Get the version of TDNF installed on the system""" self.composite_logger.log_debug("[TDNF] Getting tdnf version...") - cmd = "rpm -q tdnf | sed -E 's/tdnf-([0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+).*/\\1/'" + cmd = "rpm -q tdnf | sed -E 's/^tdnf-([0-9]+\\.[0-9]+\\.[0-9]+-[0-9]+\\.[a-zA-Z0-9]+).*/\\1/'" code, output = self.env_layer.run_command_output(cmd, False, False) if code == 0: - # Sample output: 3.5.8-3 + # Sample output: 3.5.8-3-azl3 version = output.split()[0] if output else None - self.composite_logger.log_debug("[TDNF] Installed TDNF version [Version={0}]".format(version)) + self.composite_logger.log_debug("[TDNF] TDNF version detected. [Version={0}]".format(version)) return version else: self.composite_logger.log_error("[TDNF] Failed to get TDNF version. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output)) return None - def attempt_tdnf_update_to_meet_strict_sdp_requirements(self): + def try_tdnf_update_to_meet_strict_sdp_requirements(self): + # type: () -> bool """Attempt to update TDNF to meet the minimum version required for strict SDP""" self.composite_logger.log_debug("[TDNF] Attempting to update TDNF to meet strict safe deployment requirements...") - cmd = "sudo tdnf -y install tdnf-" + Constants.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP + cmd = "sudo tdnf -y install tdnf-" + self.azl3_tdnf_packagemanager.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP code, output = self.env_layer.run_command_output(cmd, no_output=True, chk_err=False) if code == 0: - self.composite_logger.log_debug("[TDNF] Successfully updated TDNF. [Command={0}][Code={1}]".format(cmd, code)) + self.composite_logger.log_debug("[TDNF] Successfully updated TDNF for Strict SDP. [Command={0}][Code={1}]".format(cmd, code)) return True else: - self.composite_logger.log_error("[TDNF] Failed to update TDNF. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output)) + self.composite_logger.log_error("[TDNF] Failed to update TDNF for Strict SDP. [Command={0}][Code={1}][Output={2}]".format(cmd, code, output)) return False # endregion @@ -406,10 +407,9 @@ def get_dependent_list(self, packages): package_names += package cmd = self.single_package_upgrade_simulation_cmd + package_names - self.composite_logger.log_verbose("[TDNF] Resolving dependencies. [Command={0}]".format(str(cmd))) output = self.invoke_package_manager(cmd) dependencies = self.extract_dependencies(output, packages) - self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Packages={0}][DependencyCount={1}]".format(str(packages), len(dependencies))) + self.composite_logger.log_verbose("[TDNF] Resolved dependencies. [Command={0}][Packages={1}][DependencyCount={2}]".format(str(cmd),str(packages), len(dependencies))) return dependencies def get_product_name(self, package_name): @@ -829,3 +829,9 @@ def separate_out_esm_packages(self, packages, package_versions): def get_package_install_expected_avg_time_in_seconds(self): return self.package_install_expected_avg_time_in_seconds + # region - AzLinux specilizations + class AzL3TdnfPackageManager(object): + """AzLinux Package Manager class for TDNF package manager.""" + def __init__(self): + self.TDNF_MINIMUM_VERSION_FOR_STRICT_SDP = "3.5.8-3.azl3" # minimum version of tdnf required to support Strict SDP in Azure Linux + diff --git a/src/core/src/package_managers/YumPackageManager.py b/src/core/src/package_managers/YumPackageManager.py index 1608b41a..9998bb2c 100644 --- a/src/core/src/package_managers/YumPackageManager.py +++ b/src/core/src/package_managers/YumPackageManager.py @@ -268,7 +268,8 @@ def install_security_updates_azgps_coordinated(self): pass def meets_azgps_coordinated_requirements(self): - return + # type: () -> bool + return False # endregion # region Package Information diff --git a/src/core/src/package_managers/ZypperPackageManager.py b/src/core/src/package_managers/ZypperPackageManager.py index c1635f08..43e5c4dc 100644 --- a/src/core/src/package_managers/ZypperPackageManager.py +++ b/src/core/src/package_managers/ZypperPackageManager.py @@ -422,7 +422,8 @@ def install_security_updates_azgps_coordinated(self): pass def meets_azgps_coordinated_requirements(self): - return + # type: () -> bool + return False # endregion # region Package Information diff --git a/src/core/tests/Test_EnvLayer.py b/src/core/tests/Test_EnvLayer.py index 3662951e..37db4055 100644 --- a/src/core/tests/Test_EnvLayer.py +++ b/src/core/tests/Test_EnvLayer.py @@ -106,21 +106,24 @@ def test_get_package_manager(self): platform.system = self.backup_platform_system def test_is_distro_azure_linux_3_or_beyond(self): + self.backup_linux_distribution = self.envlayer.platform.linux_distribution self.backup_envlayer_distro_os_release_attr = distro.os_release_attr test_input_output_table = [ - [self.mock_distro_os_release_attr_return_azure_linux_3, True], - [self.mock_distro_os_release_attr_return_azure_linux_2, False], - [self.mock_distro_os_release_attr_return_none, False] + [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_azure_linux_3, True], + [self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, False], + [self.mock_linux_distribution_to_return_azure_linux_3, self.mock_distro_os_release_attr_return_none, False] ] for row in test_input_output_table: - distro.os_release_attr = row[0] + self.envlayer.platform.linux_distribution = row[0] + distro.os_release_attr = row[1] result = self.envlayer.is_distro_azure_linux_3_or_beyond() - self.assertEqual(result, row[1]) + self.assertEqual(result, row[2]) # restore original methods distro.os_release_attr = self.backup_envlayer_distro_os_release_attr + self.envlayer.platform.linux_distribution = self.backup_linux_distribution def test_filesystem(self): # only validates if these invocable without exceptions diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index 509df5de..e3259b06 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -45,6 +45,9 @@ def mock_do_processes_require_restart_raise_exception(self): def mock_linux_distribution_to_return_azure_linux(self): return ['Microsoft Azure Linux', '3.0', ''] + def mock_linux_distribution_to_return_azure_linux_2(self): + return ['Common Base Linux Mariner', '2.0', ''] + def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode='a+'): raise Exception @@ -60,17 +63,20 @@ def mock_run_command_output_return_0(self, cmd, no_output=False, chk_err=True): return 0, "Successfully executed command\n" def mock_get_tdnf_version_return_tdnf_3_5_8_3(self): - return "3.5.8-3" + return "3.5.8-3.azl3" def mock_get_tdnf_version_return_tdnf_4_0(self): - return "4.0.0-1" + return "4.0.0-1.azl3" def mock_get_tdnf_version_return_tdnf_2_5(self): - return "2.5.0-1" + return "2.5.0-1.cm2" def mock_get_tdnf_version_return_tdnf_3_5_8_2(self): return "3.5.8-2.azl3" + def mock_get_tdnf_version_return_tdnf_3_5_8_6_cm2(self): + return "3.5.8-6.cm2" + def mock_get_tdnf_version_return_None(self): return None @@ -928,11 +934,12 @@ def test_is_mininum_tdnf_version_for_strict_sdp_installed(self): self.backup_get_tdnf_version = package_manager.get_tdnf_version test_input_output_table = [ + [self.mock_get_tdnf_version_return_None, False], [self.mock_get_tdnf_version_return_tdnf_2_5, False], [self.mock_get_tdnf_version_return_tdnf_3_5_8_2, False], + [self.mock_get_tdnf_version_return_tdnf_3_5_8_6_cm2, False], [self.mock_get_tdnf_version_return_tdnf_3_5_8_3, True], - [self.mock_get_tdnf_version_return_tdnf_4_0, True], - [self.mock_get_tdnf_version_return_None, False] + [self.mock_get_tdnf_version_return_tdnf_4_0, True] ] for row in test_input_output_table: @@ -942,7 +949,7 @@ def test_is_mininum_tdnf_version_for_strict_sdp_installed(self): package_manager.get_tdnf_version = self.backup_get_tdnf_version - def test_attempt_tdnf_update_to_meet_strict_sdp_requirements(self): + def test_try_tdnf_update_to_meet_strict_sdp_requirements(self): package_manager = self.container.get('package_manager') self.assertTrue(package_manager is not None) @@ -955,7 +962,7 @@ def test_attempt_tdnf_update_to_meet_strict_sdp_requirements(self): for row in input_output_table: self.runtime.env_layer.run_command_output = row[0] - result = package_manager.attempt_tdnf_update_to_meet_strict_sdp_requirements() + result = package_manager.try_tdnf_update_to_meet_strict_sdp_requirements() self.assertEqual(result, row[1]) self.runtime.env_layer.run_command_output = self.backup_run_command_output @@ -965,6 +972,7 @@ def test_meets_azgps_coordinated_requirements(self): self.assertTrue(package_manager is not None) # backup methods + self.backup_linux_distribution = self.runtime.env_layer.platform.linux_distribution self.backup_distro_os_release_attr = distro.os_release_attr self.backup_get_tdnf_version = package_manager.get_tdnf_version self.backup_run_command_output = self.runtime.env_layer.run_command_output @@ -976,22 +984,29 @@ def test_meets_azgps_coordinated_requirements(self): 4. Azure Linux 3 with tdnf version < 3.5.8-3, will not be updated to 3.5.8-3 5. Azure Linux 2""" test_input_output_table = [ - [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_4_0, self.backup_run_command_output, True], - [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_3_5_8_3, self.backup_run_command_output, True], - [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_0, True], - [self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_1, False], - [self.mock_distro_os_release_attr_return_azure_linux_2, self.backup_distro_os_release_attr, self.backup_run_command_output, False] + [self.mock_linux_distribution_to_return_azure_linux, self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_4_0, self.backup_run_command_output, True], + [self.mock_linux_distribution_to_return_azure_linux, self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_3_5_8_3, self.backup_run_command_output, True], + [self.mock_linux_distribution_to_return_azure_linux, self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_0, True], + [self.mock_linux_distribution_to_return_azure_linux, self.mock_distro_os_release_attr_return_azure_linux_3, self.mock_get_tdnf_version_return_tdnf_2_5, self.mock_run_command_output_return_1, False], + [self.mock_linux_distribution_to_return_azure_linux_2, self.mock_distro_os_release_attr_return_azure_linux_2, self.backup_distro_os_release_attr, self.backup_run_command_output, False] ] for row in test_input_output_table: # set test case values - distro.os_release_attr = row[0] - package_manager.get_tdnf_version = row[1] - self.runtime.env_layer.run_command_output = row[2] + self.runtime.env_layer.platform.linux_distribution = row[0] + distro.os_release_attr = row[1] + package_manager.get_tdnf_version = row[2] + self.runtime.env_layer.run_command_output = row[3] # run test case result = package_manager.meets_azgps_coordinated_requirements() - self.assertEqual(result, row[3]) + self.assertEqual(result, row[4]) + + # restore original methods + self.runtime.env_layer.platform.linux_distribution = self.backup_linux_distribution + distro.os_release_attr = self.backup_distro_os_release_attr + package_manager.get_tdnf_version = self.backup_get_tdnf_version + self.runtime.env_layer.run_command_output = self.backup_run_command_output if __name__ == '__main__': diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index dd46dd7f..967ab32e 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -628,7 +628,7 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): output = 'Auto update service disabled' elif cmd.find("rpm -q tdnf") > -1: code = 0 - output = '3.5.8-3' + output = '3.5.8-3.azl3' elif cmd.find('tdnf -y upgrade') > -1: code = 0 output = "Loaded plugin: tdnfrepogpgcheck\n" + \ From 3f56dc064994f8370d2d8fffcc76f24226cd88b5 Mon Sep 17 00:00:00 2001 From: Rajasi Rane Date: Thu, 21 Aug 2025 19:25:05 -0700 Subject: [PATCH 13/13] AzureLinux Strict SDP support: Addressing PR comments #5 --- src/core/src/bootstrap/Constants.py | 2 +- src/core/src/core_logic/PatchInstaller.py | 2 +- .../src/package_managers/AptitudePackageManager.py | 2 +- src/core/src/package_managers/PackageManager.py | 2 +- src/core/src/package_managers/TdnfPackageManager.py | 10 +++++----- src/core/src/package_managers/YumPackageManager.py | 2 +- src/core/src/package_managers/ZypperPackageManager.py | 2 +- src/core/tests/Test_CoreMain.py | 2 +- src/core/tests/Test_TdnfPackageManager.py | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 84598e22..e3e21323 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -347,7 +347,7 @@ class TelemetryTaskName(EnumBackport): TELEMETRY_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Azure Linux Agent version. To resolve: http://aka.ms/UpdateLinuxAgent" TELEMETRY_COMPATIBLE_MSG = "Minimum Azure Linux Agent version prerequisite met" PYTHON_NOT_COMPATIBLE_ERROR_MSG = "Unsupported older Python version. Minimum Python version required is 2.7. [DetectedPythonVersion={0}]" - INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" # aka.ms link to be distro-agnostic + INFO_STRICT_SDP_SUCCESS = "Success: Safely patched your VM in a AzGPS-coordinated global rollout. https://aka.ms/AzGPS/StrictSDP [Target={0}]" UTC_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # EnvLayer Constants diff --git a/src/core/src/core_logic/PatchInstaller.py b/src/core/src/core_logic/PatchInstaller.py index 30da7b35..16a26adc 100644 --- a/src/core/src/core_logic/PatchInstaller.py +++ b/src/core/src/core_logic/PatchInstaller.py @@ -80,7 +80,7 @@ def start_installation(self, simulate=False): if self.execution_config.max_patch_publish_date != str(): self.package_manager.set_max_patch_publish_date(self.execution_config.max_patch_publish_date) - if self.package_manager.max_patch_publish_date != str() and self.package_manager.meets_azgps_coordinated_requirements(): + if self.package_manager.max_patch_publish_date != str() and self.package_manager.try_meet_azgps_coordinated_requirements(): """ Strict SDP with the package manager that supports it """ installed_update_count, update_run_successful, maintenance_window_exceeded = self.install_updates_azgps_coordinated(maintenance_window, package_manager, simulate) package_manager.set_package_manager_setting(Constants.PACKAGE_MGR_SETTING_REPEAT_PATCH_OPERATION, bool(not update_run_successful)) diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index 4ae5d90c..cdc1b2ec 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -489,7 +489,7 @@ def install_security_updates_azgps_coordinated(self): out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) return code, out - def meets_azgps_coordinated_requirements(self): + def try_meet_azgps_coordinated_requirements(self): # type: () -> bool return True # endregion diff --git a/src/core/src/package_managers/PackageManager.py b/src/core/src/package_managers/PackageManager.py index c6ae472a..d2bb57d3 100644 --- a/src/core/src/package_managers/PackageManager.py +++ b/src/core/src/package_managers/PackageManager.py @@ -348,7 +348,7 @@ def install_security_updates_azgps_coordinated(self): pass @abstractmethod - def meets_azgps_coordinated_requirements(self): + def try_meet_azgps_coordinated_requirements(self): # type: () -> bool """ Returns true if the package manager meets the requirements for azgps coordinated security updates """ return False diff --git a/src/core/src/package_managers/TdnfPackageManager.py b/src/core/src/package_managers/TdnfPackageManager.py index 8ea98bb2..9c380548 100644 --- a/src/core/src/package_managers/TdnfPackageManager.py +++ b/src/core/src/package_managers/TdnfPackageManager.py @@ -96,12 +96,12 @@ def refresh_repo(self): # region Strict SDP using SnapshotTime @staticmethod - def __generate_command_with_snapshotposixtime_if_specified(command_template, snapshotposixtime=str()): + def __generate_command_with_snapshotposixtime_if_specified(command_template, snapshot_posix_time=str()): # type: (str, str) -> str - if snapshotposixtime == str(): + if snapshot_posix_time == str(): return command_template.replace('', str()) else: - return command_template.replace('', ('--snapshottime={0}'.format(str(snapshotposixtime)))) + return command_template.replace('', ('--snapshottime={0}'.format(str(snapshot_posix_time)))) # endregion # region Get Available Updates @@ -230,7 +230,7 @@ def install_security_updates_azgps_coordinated(self): out, code = self.invoke_package_manager_advanced(command, raise_on_exception=False) return code, out - def meets_azgps_coordinated_requirements(self): + def try_meet_azgps_coordinated_requirements(self): # type: () -> bool """ Check if the system meets the requirements for Azure Linux strict safe deployment and attempt to update TDNF if necessary """ self.composite_logger.log_debug("[TDNF] Checking if system meets Azure Linux security updates requirements...") @@ -829,7 +829,7 @@ def separate_out_esm_packages(self, packages, package_versions): def get_package_install_expected_avg_time_in_seconds(self): return self.package_install_expected_avg_time_in_seconds - # region - AzLinux specilizations + # region - AzLinux specializations class AzL3TdnfPackageManager(object): """AzLinux Package Manager class for TDNF package manager.""" def __init__(self): diff --git a/src/core/src/package_managers/YumPackageManager.py b/src/core/src/package_managers/YumPackageManager.py index 9998bb2c..9e99cd41 100644 --- a/src/core/src/package_managers/YumPackageManager.py +++ b/src/core/src/package_managers/YumPackageManager.py @@ -267,7 +267,7 @@ def install_updates_fail_safe(self, excluded_packages): def install_security_updates_azgps_coordinated(self): pass - def meets_azgps_coordinated_requirements(self): + def try_meet_azgps_coordinated_requirements(self): # type: () -> bool return False # endregion diff --git a/src/core/src/package_managers/ZypperPackageManager.py b/src/core/src/package_managers/ZypperPackageManager.py index 43e5c4dc..365eb1da 100644 --- a/src/core/src/package_managers/ZypperPackageManager.py +++ b/src/core/src/package_managers/ZypperPackageManager.py @@ -421,7 +421,7 @@ def install_updates_fail_safe(self, excluded_packages): def install_security_updates_azgps_coordinated(self): pass - def meets_azgps_coordinated_requirements(self): + def try_meet_azgps_coordinated_requirements(self): # type: () -> bool return False # endregion diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index a8e9f0ba..2071171e 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -24,10 +24,10 @@ from core.src.CoreMain import CoreMain from core.src.bootstrap.Constants import Constants +from core.src.external_dependencies import distro from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions from core.tests.library.RuntimeCompositor import RuntimeCompositor -from core.src.external_dependencies import distro class TestCoreMain(unittest.TestCase): diff --git a/src/core/tests/Test_TdnfPackageManager.py b/src/core/tests/Test_TdnfPackageManager.py index e3259b06..c05275de 100644 --- a/src/core/tests/Test_TdnfPackageManager.py +++ b/src/core/tests/Test_TdnfPackageManager.py @@ -967,7 +967,7 @@ def test_try_tdnf_update_to_meet_strict_sdp_requirements(self): self.runtime.env_layer.run_command_output = self.backup_run_command_output - def test_meets_azgps_coordinated_requirements(self): + def test_try_meet_azgps_coordinated_requirements(self): package_manager = self.container.get('package_manager') self.assertTrue(package_manager is not None) @@ -999,7 +999,7 @@ def test_meets_azgps_coordinated_requirements(self): self.runtime.env_layer.run_command_output = row[3] # run test case - result = package_manager.meets_azgps_coordinated_requirements() + result = package_manager.try_meet_azgps_coordinated_requirements() self.assertEqual(result, row[4]) # restore original methods