From 4b4a5a26d90cd9ed2ab9ae862c87694d036223ba Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Wed, 8 Jul 2026 11:49:09 -0400 Subject: [PATCH 1/6] Initial change --- .../package_managers/Dnf5PackageManager.py | 310 +++++++++++++++--- src/core/tests/Test_Dnf5PackageManager.py | 2 +- 2 files changed, 265 insertions(+), 47 deletions(-) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index d2942a1c..ed2cb39b 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -59,7 +59,8 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ # auto OS updates self.current_auto_os_update_service = None - self.os_patch_configuration_settings_file_path = '' + self.os_patch_default_configuration_settings_file_path = '' + self.os_patch_override_configuration_settings_file_path = '' self.auto_update_service_enabled = False self.auto_update_config_pattern_match_text = "" self.download_updates_identifier_text = "" @@ -360,13 +361,17 @@ def __init_constants_for_dnf5_automatic(self): self.dnf5_automatic_enable_on_reboot_check_cmd = 'systemctl is-enabled dnf5-automatic.timer' self.dnf5_automatic_disable_on_reboot_cmd = 'systemctl disable --now dnf5-automatic.timer' self.dnf5_automatic_enable_on_reboot_cmd = 'systemctl enable --now dnf5-automatic.timer' - self.dnf5_automatic_configuration_file_path = '/etc/dnf/automatic.conf' + self.dnf5_automatic_default_configuration_file_path = '/usr/share/dnf5/dnf5-plugins/automatic.conf' + self.dnf5_automatic_override_configuration_file_path = '/etc/dnf/automatic.conf' self.dnf5_automatic_config_pattern_match_text = ' = (no|yes)' self.dnf5_automatic_download_updates_identifier_text = "download_updates" self.dnf5_automatic_apply_updates_identifier_text = "apply_updates" self.dnf5_automatic_enable_on_reboot_identifier_text = "enable_on_reboot" self.dnf5_automatic_installation_state_identifier_text = "installation_state" self.dnf5_auto_os_update_service = "dnf5-automatic" + self.dnf5_default_auto_os_config_backup_key = "default-dnf5-automatic" + self.dnf5_override_auto_os_config_backup_key = "override-dnf5-automatic" + self.dnf5_automatic_remove_override_configuration_file_cmd = 'rm -f /etc/dnf/automatic.conf' def get_current_auto_os_patch_state(self): """ Gets the current auto OS update patch state on the machine """ @@ -401,7 +406,8 @@ def __get_current_auto_os_patch_state_for_dnf5_automatic(self): return Constants.AutomaticOSPatchStates.UNKNOWN def __init_auto_update_for_dnf5_automatic(self): - self.os_patch_configuration_settings_file_path = self.dnf5_automatic_configuration_file_path + self.os_patch_default_configuration_settings_file_path = self.dnf5_automatic_default_configuration_file_path + self.os_patch_override_configuration_settings_file_path = self.dnf5_automatic_override_configuration_file_path self.auto_update_config_pattern_match_text = self.dnf5_automatic_config_pattern_match_text self.download_updates_identifier_text = self.dnf5_automatic_download_updates_identifier_text self.apply_updates_identifier_text = self.dnf5_automatic_apply_updates_identifier_text @@ -411,6 +417,8 @@ def __init_auto_update_for_dnf5_automatic(self): self.enable_on_reboot_cmd = self.dnf5_automatic_enable_on_reboot_cmd self.install_check_cmd = self.dnf5_automatic_install_check_cmd self.current_auto_os_update_service = self.dnf5_auto_os_update_service + self.os_patch_default_configuration_backup_key = self.dnf5_default_auto_os_config_backup_key + self.os_patch_override_configuration_backup_key = self.dnf5_override_auto_os_config_backup_key def __get_current_auto_os_updates_setting_on_machine(self): """Gets all auto-OS update settings for dnf5-automatic (DNF5) via config + timer state.""" @@ -437,17 +445,15 @@ def __get_current_auto_os_updates_setting_on_machine(self): enable_on_reboot_value = self.is_service_set_to_enable_on_reboot(self.enable_on_reboot_check_cmd) self.composite_logger.log_verbose("[DNF5] Checking if auto updates are currently enabled...") - image_default_patch_configuration = self.env_layer.file_system.read_with_retry(self.os_patch_configuration_settings_file_path, raise_if_not_found=False) - if image_default_patch_configuration is not None: - settings = image_default_patch_configuration.strip().split('\n') - for setting in settings: - match = re.search(self.download_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) - if match is not None: - download_updates_value = match.group(1) - - match = re.search(self.apply_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) - if match is not None: - apply_updates_value = match.group(1) + default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() + + download_updates_value = (override_download_updates_value if override_download_updates_value != "" else default_download_updates_value) + + apply_updates_value = ( + override_apply_updates_value + if override_apply_updates_value != "" + else default_apply_updates_value + ) if download_updates_value == "": self.composite_logger.log_verbose("[DNF5] Machine did not have any value set for [Setting={0}]".format(str(self.download_updates_identifier_text))) @@ -501,7 +507,6 @@ def __disable_auto_os_update_for_dnf5_automatic(self): """ Disables auto OS updates, using dnf5-automatic service, and logs the default settings the machine comes with """ self.composite_logger.log_verbose("[DNF5] Disabling auto OS updates using dnf5-automatic") self.__init_auto_update_for_dnf5_automatic() - self.backup_image_default_patch_configuration_if_not_exists() if not self.is_auto_update_service_installed(self.dnf5_automatic_install_check_cmd): @@ -509,11 +514,22 @@ def __disable_auto_os_update_for_dnf5_automatic(self): return self.composite_logger.log_verbose("[DNF5] Preemptively disabling auto OS updates using dnf5-automatic") + # Check if override.conf file exists, if not copy/create from default config location + self.__ensure_override_configuration_exists() self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, "no", self.dnf5_automatic_config_pattern_match_text) self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, "no", self.dnf5_automatic_config_pattern_match_text) self.disable_auto_update_on_reboot(self.dnf5_automatic_disable_on_reboot_cmd) self.composite_logger.log_debug("[DNF5] Successfully disabled auto OS updates using dnf5-automatic") + def __ensure_override_configuration_exists(self): + override_config_file = self.env_layer.file_system.read_with_retry(self.os_patch_override_configuration_settings_file_path, raise_if_not_found=False) + if override_config_file is not None: + return + + self.composite_logger.log_debug("[DNF5] Override configuration file does not exist.Creating it from default configuration.") + default_config = self.env_layer.file_system.read_with_retry(self.os_patch_default_configuration_settings_file_path) + self.env_layer.file_system.write_with_retry(self.os_patch_override_configuration_settings_file_path, default_config, mode='w+') + def disable_auto_update_on_reboot(self, command): """ Disables auto update on reboot by executing systemctl command """ self.composite_logger.log_verbose("[DNF5] Disabling auto update on reboot. [Command={0}] ".format(command)) @@ -532,11 +548,17 @@ def backup_image_default_patch_configuration_if_not_exists(self): Log the default system settings a VM comes with, any subsequent updates will not be recorded""" """ JSON format for backup file: { - "dnf5-automatic": { - "apply_updates": "yes/no/empty string", - "download_updates": "yes/no/empty string", - "enable_on_reboot": true/false, - "installation_state": true/false + "default-dnf5-automatic": { + "apply_updates": "yes/no/empty string", + "download_updates": "yes/no/empty string", + "enable_on_reboot": true/false, + "installation_state": true/false + }, + "override-dnf5-automatic": { + "apply_updates": "yes/no/empty string", + "download_updates": "yes/no/empty string", + "enable_on_reboot": true/false, + "installation_state": true/false } } """ try: @@ -552,12 +574,20 @@ def backup_image_default_patch_configuration_if_not_exists(self): self.composite_logger.log_debug("[DNF5] Since the backup is invalid, will add a new backup with the current auto OS update settings") self.composite_logger.log_verbose("[DNF5] Fetching current auto OS update settings for [AutoOSUpdateService={0}]".format(str(self.current_auto_os_update_service))) - is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value = self.__get_current_auto_os_updates_setting_on_machine() + is_service_installed, enable_on_reboot_value, _, _ = self.__get_current_auto_os_updates_setting_on_machine() + + default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() backup_image_default_patch_configuration_json_to_add = { - self.current_auto_os_update_service: { - self.download_updates_identifier_text: download_updates_value, - self.apply_updates_identifier_text: apply_updates_value, + self.os_patch_default_configuration_backup_key: { + self.download_updates_identifier_text: default_download_updates_value, + self.apply_updates_identifier_text: default_apply_updates_value, + self.enable_on_reboot_identifier_text: enable_on_reboot_value, + self.installation_state_identifier_text: is_service_installed + }, + self.os_patch_override_configuration_backup_key: { + self.download_updates_identifier_text: override_download_updates_value, + self.apply_updates_identifier_text: override_apply_updates_value, self.enable_on_reboot_identifier_text: enable_on_reboot_value, self.installation_state_identifier_text: is_service_installed } @@ -572,28 +602,90 @@ def backup_image_default_patch_configuration_if_not_exists(self): self.status_handler.add_error_to_status("[DNF5] Exception during fetching and logging default auto update settings on the machine. [Exception={0}]".format(repr(error)), Constants.PatchOperationErrorCodes.DEFAULT_ERROR) raise + def __get_default_and_override_config_values(self): + self.composite_logger.log_debug("[DNF5] Reading default configuration file. [Path={0}]".format(self.os_patch_default_configuration_settings_file_path)) + default_download_updates_value, default_apply_updates_value = self.__get_config_values(self.os_patch_default_configuration_settings_file_path) + self.composite_logger.log_debug("[DNF5] Default configuration values found.[download_updates={0}][apply_updates={1}]".format(default_download_updates_value, default_apply_updates_value)) + + self.composite_logger.log_debug("[DNF5] Reading override configuration file. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) + override_download_updates_value, override_apply_updates_value = self.__get_config_values(self.os_patch_override_configuration_settings_file_path) + self.composite_logger.log_debug("[DNF5] Override configuration values found.[download_updates={0}][apply_updates={1}]".format(override_download_updates_value, override_apply_updates_value)) + + return default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value + + def __get_config_values(self, config_file_path): + download_updates_value = "" + apply_updates_value = "" + + self.composite_logger.log_debug("[DNF5] Reading config file. [Path={0}]".format(config_file_path)) + config = self.env_layer.file_system.read_with_retry(config_file_path, raise_if_not_found=False) + if config is None: + self.composite_logger.log_debug("[DNF5] Config file not found. [Path={0}]".format(config_file_path)) + return download_updates_value, apply_updates_value + + settings = config.strip().split('\n') + + for setting in settings: + self.composite_logger.log_verbose("[DNF5] Reading config file.") + match = re.search(self.download_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) + if match is not None: + download_updates_value = match.group(1) + self.composite_logger.log_debug("[DNF5] Found download_updates setting.[Value={0}]".format(download_updates_value)) + + match = re.search(self.apply_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) + if match is not None: + apply_updates_value = match.group(1) + self.composite_logger.log_debug("[DNF5] Found apply_updates setting.[Value={0}]".format(apply_updates_value)) + + self.composite_logger.log_debug("[DNF5] Finished parsing configuration values.[Path={0}][DownloadUpdates={1}][ApplyUpdates={2}]".format(config_file_path, download_updates_value, apply_updates_value)) + + return download_updates_value, apply_updates_value + def is_image_default_patch_configuration_backup_valid(self, image_default_patch_configuration_backup): """ Verifies if default auto update configurations, for a service under consideration, are saved in backup """ return self.is_backup_valid_for_dnf5_automatic(image_default_patch_configuration_backup) def is_backup_valid_for_dnf5_automatic(self, image_default_patch_configuration_backup): - if self.dnf5_auto_os_update_service in image_default_patch_configuration_backup \ - and self.dnf5_automatic_download_updates_identifier_text in image_default_patch_configuration_backup[self.dnf5_auto_os_update_service] \ - and self.dnf5_automatic_apply_updates_identifier_text in image_default_patch_configuration_backup[self.dnf5_auto_os_update_service] \ - and self.dnf5_automatic_enable_on_reboot_identifier_text in image_default_patch_configuration_backup[self.dnf5_auto_os_update_service] \ - and self.dnf5_automatic_installation_state_identifier_text in image_default_patch_configuration_backup[self.dnf5_auto_os_update_service]: - self.composite_logger.log_debug("[DNF5] Extension has a valid backup for default dnf5-automatic configuration settings") - return True - else: - self.composite_logger.log_debug("[DNF5] Extension does not have a valid backup for default dnf5-automatic configuration settings") + default_backup_valid = self.__is_backup_valid( + image_default_patch_configuration_backup, + self.os_patch_default_configuration_backup_key + ) + + override_backup_valid = self.__is_backup_valid( + image_default_patch_configuration_backup, + self.os_patch_override_configuration_backup_key + ) + + if default_backup_valid and override_backup_valid: + self.composite_logger.log_debug( + "[DNF5] Extension has a valid backup for default and override dnf5-automatic configuration settings" + ) + return True + + self.composite_logger.log_debug( + "[DNF5] Extension does not have a valid backup for default and override dnf5-automatic configuration settings" + ) return False - def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="no",config_pattern_match_text=""): + def __is_backup_valid(self, image_default_patch_configuration_backup, backup_key): + return (backup_key in image_default_patch_configuration_backup + and self.dnf5_automatic_download_updates_identifier_text + in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_apply_updates_identifier_text + in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_enable_on_reboot_identifier_text + in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_installation_state_identifier_text + in image_default_patch_configuration_backup[backup_key]) + + def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="no",config_pattern_match_text="", config_file_path=None): try: # note: adding space between the patch_configuration_sub_setting and value since, we will have to do that if we have to add a patch_configuration_sub_setting that did not exist before + if config_file_path is None: + config_file_path = self.os_patch_override_configuration_settings_file_path self.composite_logger.log_debug("[DNF5] Updating system configuration settings for auto OS updates. [Patch Configuration Sub Setting={0}] [Value={1}]".format( str(patch_configuration_sub_setting), value)) - os_patch_configuration_settings = self.env_layer.file_system.read_with_retry(self.os_patch_configuration_settings_file_path) + os_patch_configuration_settings = self.env_layer.file_system.read_with_retry(config_file_path) patch_configuration_sub_setting_to_update = patch_configuration_sub_setting + ' = ' + value patch_configuration_sub_setting_found_in_file = False updated_patch_configuration_sub_setting = "" @@ -610,7 +702,7 @@ def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_sett if not patch_configuration_sub_setting_found_in_file: updated_patch_configuration_sub_setting += patch_configuration_sub_setting_to_update + "\n" - self.env_layer.file_system.write_with_retry(self.os_patch_configuration_settings_file_path,'{0}'.format(updated_patch_configuration_sub_setting.lstrip()),mode='w+') + self.env_layer.file_system.write_with_retry(config_file_path,'{0}'.format(updated_patch_configuration_sub_setting.lstrip()),mode='w+') except Exception as error: error_msg = "[DNF5] Error occurred while updating system configuration settings for auto OS updates. [Patch Configuration={0}] [Error={1}]".format( str(patch_configuration_sub_setting), repr(error)) @@ -636,25 +728,151 @@ def revert_auto_os_update_to_system_default_for_dnf5_automatic(self): self.composite_logger.log_debug("[DNF5] Machine default auto OS update service is not installed on the VM and hence no config to revert. [Service={0}]".format(str(self.current_auto_os_update_service))) return - self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates [Service={0}][Is_Service_Installed={1}][Machine_default_update_enable_on_reboot={2}]".format( - str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value))) + default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() + self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates " + "[Service={0}]""[Is_Service_Installed={1}]""[Machine_default_update_enable_on_reboot={2}]""[Effective_download_updates={3}]" + "[Effective_apply_updates={4}]" + "[Default_download_updates={5}]""[Default_apply_updates={6}]""[Override_download_updates={7}]""[Override_apply_updates={8}]" + .format(str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value), + str(download_updates_value), + str(apply_updates_value), + str(default_download_updates_value), + str(default_apply_updates_value), + str(override_download_updates_value), + str(override_apply_updates_value))) + # self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates [Service={0}][Is_Service_Installed={1}][Machine_default_update_enable_on_reboot={2}]".format( + # str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value))) image_default_patch_configuration_backup = self.__get_image_default_patch_configuration_backup() self.composite_logger.log_verbose("[DNF5] Logging system default configuration settings for auto OS updates. [Settings={0}]".format(str(image_default_patch_configuration_backup))) is_backup_valid = self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup) if is_backup_valid: - download_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.download_updates_identifier_text] - apply_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.apply_updates_identifier_text] - enable_on_reboot_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.enable_on_reboot_identifier_text] - - self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, download_updates_value_from_backup, self.auto_update_config_pattern_match_text) - self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, apply_updates_value_from_backup, self.auto_update_config_pattern_match_text) - if str(enable_on_reboot_value_from_backup).lower() == 'true': - self.enable_auto_update_on_reboot() + default_backup = image_default_patch_configuration_backup[self.os_patch_default_configuration_backup_key] + override_backup = image_default_patch_configuration_backup[self.os_patch_override_configuration_backup_key] + + self.__restore_default_configuration_from_backup(default_backup) + self.__restore_override_configuration_from_backup(override_backup) + self.__restore_enable_on_reboot_state_from_backup(default_backup) + # download_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.download_updates_identifier_text] + # apply_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.apply_updates_identifier_text] + # enable_on_reboot_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.enable_on_reboot_identifier_text] + # + # self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, download_updates_value_from_backup, self.auto_update_config_pattern_match_text) + # self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, apply_updates_value_from_backup, self.auto_update_config_pattern_match_text) + # if str(enable_on_reboot_value_from_backup).lower() == 'true': + # self.enable_auto_update_on_reboot() else: self.composite_logger.log_debug("[DNF5] Since the backup is invalid or does not exist for current service, we won't be able to revert auto OS patch settings to their system default value. [Service={0}]".format(str(self.current_auto_os_update_service))) + def __remove_override_configuration_if_exists(self): + """Removes dnf5-automatic override configuration file if it exists.Missing override file is valid by design, so this method must not throw + when the file is absent.""" + override_config_file = self.env_layer.file_system.read_with_retry(self.os_patch_override_configuration_settings_file_path, raise_if_not_found=False) + + if override_config_file is None: + self.composite_logger.log_debug("[DNF5] Override configuration file does not exist. Nothing to remove. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) + return + + self.composite_logger.log_debug("[DNF5] Removing override configuration file to restore machine default. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path) ) + + code, out = self.env_layer.run_command_output(self.dnf5_automatic_remove_override_configuration_file_cmd, False, False) + + if code != 0: + error_msg = "[DNF5] Error removing override configuration file. [Command={0}][Code={1}][Output={2}]".format( + self.dnf5_automatic_remove_override_configuration_file_cmd, + str(code), + out + ) + self.composite_logger.log_error(error_msg) + self.status_handler.add_error_to_status( + error_msg, + Constants.PatchOperationErrorCodes.OPERATION_FAILED + ) + raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) + + self.composite_logger.log_debug( + "[DNF5] Removed override configuration file. [Command={0}][Code={1}][Output={2}]" + .format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out) + ) + + def __restore_default_configuration_from_backup(self, default_backup): + """Restore default dnf5-automatic configuration to its backed up state.""" + + default_download_updates = default_backup[self.download_updates_identifier_text] + default_apply_updates = default_backup[self.apply_updates_identifier_text] + + self.composite_logger.log_debug("[DNF5] Restoring default dnf5-automatic configuration values from backup.[Path={0}][download_updates={1}][apply_updates={2}]" + .format(self.os_patch_default_configuration_settings_file_path, str(default_download_updates),str(default_apply_updates) + ) + ) + + self.update_os_patch_configuration_sub_setting( + self.download_updates_identifier_text, + default_download_updates, + self.auto_update_config_pattern_match_text, + self.os_patch_default_configuration_settings_file_path + ) + + self.update_os_patch_configuration_sub_setting( + self.apply_updates_identifier_text, + default_apply_updates, + self.auto_update_config_pattern_match_text, + self.os_patch_default_configuration_settings_file_path + ) + + def __restore_override_configuration_from_backup(self, override_backup): + """Restore override dnf5-automatic configuration to its backed up state.""" + + override_download_updates = override_backup[self.download_updates_identifier_text] + override_apply_updates = override_backup[self.apply_updates_identifier_text] + + # Empty values indicate override file did not exist before onboarding. + if override_download_updates == "" and override_apply_updates == "": + self.composite_logger.log_debug( + "[DNF5] Override dnf5-automatic configuration did not exist before onboarding. " + "Removing override configuration file if it exists." + ) + + self.__remove_override_configuration_if_exists() + return + + self.composite_logger.log_debug( + "[DNF5] Restoring override dnf5-automatic configuration values from backup. " + "[Path={0}][download_updates={1}][apply_updates={2}]" + .format( + self.os_patch_override_configuration_settings_file_path, + str(override_download_updates), + str(override_apply_updates) + ) + ) + + self.__ensure_override_configuration_exists() + + self.update_os_patch_configuration_sub_setting( + self.download_updates_identifier_text, + override_download_updates, + self.auto_update_config_pattern_match_text, + self.os_patch_override_configuration_settings_file_path + ) + + self.update_os_patch_configuration_sub_setting( + self.apply_updates_identifier_text, + override_apply_updates, + self.auto_update_config_pattern_match_text, + self.os_patch_override_configuration_settings_file_path + ) + + def __restore_enable_on_reboot_state_from_backup(self, default_backup): + enable_on_reboot_value = default_backup[self.enable_on_reboot_identifier_text] + + if str(enable_on_reboot_value).lower() == 'true': + self.composite_logger.log_debug( + "[DNF5] Restoring dnf5-automatic timer to enabled state from backup." + ) + + self.enable_auto_update_on_reboot() + def enable_auto_update_on_reboot(self): """ Enables machine default auto update on reboot """ # type () -> None diff --git a/src/core/tests/Test_Dnf5PackageManager.py b/src/core/tests/Test_Dnf5PackageManager.py index 450ac74f..a3107dc8 100644 --- a/src/core/tests/Test_Dnf5PackageManager.py +++ b/src/core/tests/Test_Dnf5PackageManager.py @@ -124,7 +124,7 @@ def test_disable_auto_os_updates_with_uninstalled_services(self): self.assertIsNotNone(image_default_patch_configuration_backup) # validating backup for dnf-automatic - self.assertIn(package_manager.dnf5_auto_os_update_service, image_default_patch_configuration_backup) + self.assertIn(package_manager.dnf5_default_auto_os_config_backup_key, image_default_patch_configuration_backup) self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_download_updates_identifier_text], "") self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_apply_updates_identifier_text], "") self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) From 3ee47c884f9c75d3f622cec96d51f3a8c1086ed2 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Fri, 10 Jul 2026 11:21:47 -0400 Subject: [PATCH 2/6] Updated commit with working changes --- src/core/src/package_managers/Dnf5PackageManager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index ed2cb39b..ef7fba96 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -626,7 +626,6 @@ def __get_config_values(self, config_file_path): settings = config.strip().split('\n') for setting in settings: - self.composite_logger.log_verbose("[DNF5] Reading config file.") match = re.search(self.download_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) if match is not None: download_updates_value = match.group(1) From a01636ec7614284301c9d299b9cbc0c6e45345a0 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Thu, 23 Jul 2026 12:21:39 -0400 Subject: [PATCH 3/6] Auto OS bug fix --- .../package_managers/Dnf5PackageManager.py | 191 ++++---------- src/core/tests/Test_Dnf5PackageManager.py | 235 ++++++++++++------ .../tests/library/LegacyEnvLayerExtensions.py | 16 ++ 3 files changed, 228 insertions(+), 214 deletions(-) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index ef7fba96..149ed080 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -310,7 +310,7 @@ def extract_dependencies(self, output, packages): # Remove input packages (support both pkg and pkg.arch) if len(dependent_package_name) != 0 and dependent_package_name not in packages and dependent_package_name not in dependencies: - self.composite_logger.log_verbose("[DNF5] > Dependency detected: " + dependent_package_name) + self.composite_logger.log_debug("[DNF5] > Dependency detected: " + dependent_package_name) dependencies.append(dependent_package_name) return dependencies @@ -434,7 +434,7 @@ def __get_current_auto_os_updates_setting_on_machine(self): code, service_output = self.env_layer.run_command_output(self.dnf5_automatic_configuration_service, False, False) exec_start_line = "" - #Only print ExecStart details + # Only print ExecStart details for line in service_output.splitlines(): if line.strip().startswith("ExecStart"): exec_start_line = line.strip() @@ -448,12 +448,7 @@ def __get_current_auto_os_updates_setting_on_machine(self): default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() download_updates_value = (override_download_updates_value if override_download_updates_value != "" else default_download_updates_value) - - apply_updates_value = ( - override_apply_updates_value - if override_apply_updates_value != "" - else default_apply_updates_value - ) + apply_updates_value = (override_apply_updates_value if override_apply_updates_value != "" else default_apply_updates_value) if download_updates_value == "": self.composite_logger.log_verbose("[DNF5] Machine did not have any value set for [Setting={0}]".format(str(self.download_updates_identifier_text))) @@ -526,7 +521,7 @@ def __ensure_override_configuration_exists(self): if override_config_file is not None: return - self.composite_logger.log_debug("[DNF5] Override configuration file does not exist.Creating it from default configuration.") + self.composite_logger.log_debug("[DNF5] Override configuration file does not exist. Creating it from default configuration.") default_config = self.env_layer.file_system.read_with_retry(self.os_patch_default_configuration_settings_file_path) self.env_layer.file_system.write_with_retry(self.os_patch_override_configuration_settings_file_path, default_config, mode='w+') @@ -563,19 +558,17 @@ def backup_image_default_patch_configuration_if_not_exists(self): } """ try: self.composite_logger.log_verbose("[DNF5] Ensuring there is a backup of the default patch state for [AutoOSUpdateService={0}]".format(str(self.current_auto_os_update_service))) - image_default_patch_configuration_backup = self.__get_image_default_patch_configuration_backup() # verify if existing backup is valid if not, write to backup is_backup_valid = self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup) if is_backup_valid: - self.composite_logger.log_debug("[DNF5] Since extension has a valid backup, no need to log the current settings again. ""[Default Auto OS update settings={0}] [File path={1}]".format(str(image_default_patch_configuration_backup),self.image_default_patch_configuration_backup_path)) + self.composite_logger.log_debug("[DNF5] Since extension has a valid backup, no need to log the current settings again.[Default Auto OS update settings={0}] [File path={1}]".format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path)) else: self.composite_logger.log_debug("[DNF5] Since the backup is invalid, will add a new backup with the current auto OS update settings") self.composite_logger.log_verbose("[DNF5] Fetching current auto OS update settings for [AutoOSUpdateService={0}]".format(str(self.current_auto_os_update_service))) is_service_installed, enable_on_reboot_value, _, _ = self.__get_current_auto_os_updates_setting_on_machine() - default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() backup_image_default_patch_configuration_json_to_add = { @@ -593,23 +586,22 @@ def backup_image_default_patch_configuration_if_not_exists(self): } } image_default_patch_configuration_backup.update(backup_image_default_patch_configuration_json_to_add) - self.composite_logger.log_debug("[DNF5] Logging default system configuration settings for auto OS updates. [Settings={0}] [Log file path={1}]" - .format(str(image_default_patch_configuration_backup),self.image_default_patch_configuration_backup_path)) - self.env_layer.file_system.write_with_retry(self.image_default_patch_configuration_backup_path,'{0}'.format(json.dumps(image_default_patch_configuration_backup)),mode='w+') + .format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path)) + self.env_layer.file_system.write_with_retry(self.image_default_patch_configuration_backup_path, '{0}'.format(json.dumps(image_default_patch_configuration_backup)), mode='w+') except Exception as error: self.composite_logger.log_error("[DNF5] Exception during fetching and logging default auto update settings on the machine. [Exception={0}]".format(repr(error))) self.status_handler.add_error_to_status("[DNF5] Exception during fetching and logging default auto update settings on the machine. [Exception={0}]".format(repr(error)), Constants.PatchOperationErrorCodes.DEFAULT_ERROR) raise def __get_default_and_override_config_values(self): - self.composite_logger.log_debug("[DNF5] Reading default configuration file. [Path={0}]".format(self.os_patch_default_configuration_settings_file_path)) + self.composite_logger.log_verbose("[DNF5] Reading default configuration file. [Path={0}]".format(self.os_patch_default_configuration_settings_file_path)) default_download_updates_value, default_apply_updates_value = self.__get_config_values(self.os_patch_default_configuration_settings_file_path) - self.composite_logger.log_debug("[DNF5] Default configuration values found.[download_updates={0}][apply_updates={1}]".format(default_download_updates_value, default_apply_updates_value)) + self.composite_logger.log_verbose("[DNF5] Default configuration values found.[download_updates={0}][apply_updates={1}]".format(default_download_updates_value, default_apply_updates_value)) - self.composite_logger.log_debug("[DNF5] Reading override configuration file. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) + self.composite_logger.log_verbose("[DNF5] Reading override configuration file. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) override_download_updates_value, override_apply_updates_value = self.__get_config_values(self.os_patch_override_configuration_settings_file_path) - self.composite_logger.log_debug("[DNF5] Override configuration values found.[download_updates={0}][apply_updates={1}]".format(override_download_updates_value, override_apply_updates_value)) + self.composite_logger.log_verbose("[DNF5] Override configuration values found.[download_updates={0}][apply_updates={1}]".format(override_download_updates_value, override_apply_updates_value)) return default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value @@ -617,10 +609,10 @@ def __get_config_values(self, config_file_path): download_updates_value = "" apply_updates_value = "" - self.composite_logger.log_debug("[DNF5] Reading config file. [Path={0}]".format(config_file_path)) + self.composite_logger.log_verbose("[DNF5] Reading config file. [Path={0}]".format(config_file_path)) config = self.env_layer.file_system.read_with_retry(config_file_path, raise_if_not_found=False) if config is None: - self.composite_logger.log_debug("[DNF5] Config file not found. [Path={0}]".format(config_file_path)) + self.composite_logger.log_verbose("[DNF5] Config file not found. [Path={0}]".format(config_file_path)) return download_updates_value, apply_updates_value settings = config.strip().split('\n') @@ -629,15 +621,14 @@ def __get_config_values(self, config_file_path): match = re.search(self.download_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) if match is not None: download_updates_value = match.group(1) - self.composite_logger.log_debug("[DNF5] Found download_updates setting.[Value={0}]".format(download_updates_value)) + self.composite_logger.log_verbose("[DNF5] Found download_updates setting.[Value={0}]".format(download_updates_value)) match = re.search(self.apply_updates_identifier_text + self.auto_update_config_pattern_match_text, str(setting)) if match is not None: apply_updates_value = match.group(1) - self.composite_logger.log_debug("[DNF5] Found apply_updates setting.[Value={0}]".format(apply_updates_value)) - - self.composite_logger.log_debug("[DNF5] Finished parsing configuration values.[Path={0}][DownloadUpdates={1}][ApplyUpdates={2}]".format(config_file_path, download_updates_value, apply_updates_value)) + self.composite_logger.log_verbose("[DNF5] Found apply_updates setting.[Value={0}]".format(apply_updates_value)) + self.composite_logger.log_verbose("[DNF5] Finished parsing configuration values.[Path={0}][DownloadUpdates={1}][ApplyUpdates={2}]".format(config_file_path, download_updates_value, apply_updates_value)) return download_updates_value, apply_updates_value def is_image_default_patch_configuration_backup_valid(self, image_default_patch_configuration_backup): @@ -645,45 +636,30 @@ def is_image_default_patch_configuration_backup_valid(self, image_default_patch_ return self.is_backup_valid_for_dnf5_automatic(image_default_patch_configuration_backup) def is_backup_valid_for_dnf5_automatic(self, image_default_patch_configuration_backup): - default_backup_valid = self.__is_backup_valid( - image_default_patch_configuration_backup, - self.os_patch_default_configuration_backup_key - ) - - override_backup_valid = self.__is_backup_valid( - image_default_patch_configuration_backup, - self.os_patch_override_configuration_backup_key - ) + default_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_default_configuration_backup_key) + override_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_override_configuration_backup_key) if default_backup_valid and override_backup_valid: - self.composite_logger.log_debug( - "[DNF5] Extension has a valid backup for default and override dnf5-automatic configuration settings" - ) + self.composite_logger.log_debug("[DNF5] Extension has a valid backup for default and override dnf5-automatic configuration settings") return True - self.composite_logger.log_debug( - "[DNF5] Extension does not have a valid backup for default and override dnf5-automatic configuration settings" - ) + self.composite_logger.log_debug("[DNF5] Extension does not have a valid backup for default and override dnf5-automatic configuration settings") return False def __is_backup_valid(self, image_default_patch_configuration_backup, backup_key): return (backup_key in image_default_patch_configuration_backup - and self.dnf5_automatic_download_updates_identifier_text - in image_default_patch_configuration_backup[backup_key] - and self.dnf5_automatic_apply_updates_identifier_text - in image_default_patch_configuration_backup[backup_key] - and self.dnf5_automatic_enable_on_reboot_identifier_text - in image_default_patch_configuration_backup[backup_key] - and self.dnf5_automatic_installation_state_identifier_text - in image_default_patch_configuration_backup[backup_key]) - - def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="no",config_pattern_match_text="", config_file_path=None): + and self.dnf5_automatic_download_updates_identifier_text in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_apply_updates_identifier_text in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_enable_on_reboot_identifier_text in image_default_patch_configuration_backup[backup_key] + and self.dnf5_automatic_installation_state_identifier_text in image_default_patch_configuration_backup[backup_key]) + + def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="no", config_pattern_match_text="", config_file_path=None): try: # note: adding space between the patch_configuration_sub_setting and value since, we will have to do that if we have to add a patch_configuration_sub_setting that did not exist before if config_file_path is None: config_file_path = self.os_patch_override_configuration_settings_file_path - self.composite_logger.log_debug("[DNF5] Updating system configuration settings for auto OS updates. [Patch Configuration Sub Setting={0}] [Value={1}]".format( - str(patch_configuration_sub_setting), value)) + self.composite_logger.log_debug("[DNF5] Updating system configuration settings for auto OS updates. [Patch Configuration Sub Setting={0}] [Value={1}]" + .format(str(patch_configuration_sub_setting), value)) os_patch_configuration_settings = self.env_layer.file_system.read_with_retry(config_file_path) patch_configuration_sub_setting_to_update = patch_configuration_sub_setting + ' = ' + value patch_configuration_sub_setting_found_in_file = False @@ -701,10 +677,9 @@ def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_sett if not patch_configuration_sub_setting_found_in_file: updated_patch_configuration_sub_setting += patch_configuration_sub_setting_to_update + "\n" - self.env_layer.file_system.write_with_retry(config_file_path,'{0}'.format(updated_patch_configuration_sub_setting.lstrip()),mode='w+') + self.env_layer.file_system.write_with_retry(config_file_path,'{0}'.format(updated_patch_configuration_sub_setting.lstrip()), mode='w+') except Exception as error: - error_msg = "[DNF5] Error occurred while updating system configuration settings for auto OS updates. [Patch Configuration={0}] [Error={1}]".format( - str(patch_configuration_sub_setting), repr(error)) + error_msg = "[DNF5] Error occurred while updating system configuration settings for auto OS updates. [Patch Configuration={0}] [Error={1}]".format(str(patch_configuration_sub_setting), repr(error)) self.composite_logger.log_error(error_msg) self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.DEFAULT_ERROR) raise @@ -728,19 +703,10 @@ def revert_auto_os_update_to_system_default_for_dnf5_automatic(self): return default_download_updates_value, default_apply_updates_value, override_download_updates_value, override_apply_updates_value = self.__get_default_and_override_config_values() - self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates " - "[Service={0}]""[Is_Service_Installed={1}]""[Machine_default_update_enable_on_reboot={2}]""[Effective_download_updates={3}]" - "[Effective_apply_updates={4}]" - "[Default_download_updates={5}]""[Default_apply_updates={6}]""[Override_download_updates={7}]""[Override_apply_updates={8}]" - .format(str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value), - str(download_updates_value), - str(apply_updates_value), - str(default_download_updates_value), - str(default_apply_updates_value), - str(override_download_updates_value), - str(override_apply_updates_value))) - # self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates [Service={0}][Is_Service_Installed={1}][Machine_default_update_enable_on_reboot={2}]".format( - # str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value))) + self.composite_logger.log_verbose("[DNF5] Logging current configuration settings for auto OS updates [Service={0}][Is_Service_Installed={1}][Machine_default_update_enable_on_reboot={2}][Effective_download_updates={3}]" + "[Effective_apply_updates={4}][Default_download_updates={5}][Default_apply_updates={6}][Override_download_updates={7}][Override_apply_updates={8}]" + .format(str(self.current_auto_os_update_service), str(is_service_installed), str(enable_on_reboot_value), str(download_updates_value), str(apply_updates_value), + str(default_download_updates_value), str(default_apply_updates_value), str(override_download_updates_value), str(override_apply_updates_value))) image_default_patch_configuration_backup = self.__get_image_default_patch_configuration_backup() self.composite_logger.log_verbose("[DNF5] Logging system default configuration settings for auto OS updates. [Settings={0}]".format(str(image_default_patch_configuration_backup))) @@ -753,14 +719,6 @@ def revert_auto_os_update_to_system_default_for_dnf5_automatic(self): self.__restore_default_configuration_from_backup(default_backup) self.__restore_override_configuration_from_backup(override_backup) self.__restore_enable_on_reboot_state_from_backup(default_backup) - # download_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.download_updates_identifier_text] - # apply_updates_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.apply_updates_identifier_text] - # enable_on_reboot_value_from_backup = image_default_patch_configuration_backup[self.current_auto_os_update_service][self.enable_on_reboot_identifier_text] - # - # self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, download_updates_value_from_backup, self.auto_update_config_pattern_match_text) - # self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, apply_updates_value_from_backup, self.auto_update_config_pattern_match_text) - # if str(enable_on_reboot_value_from_backup).lower() == 'true': - # self.enable_auto_update_on_reboot() else: self.composite_logger.log_debug("[DNF5] Since the backup is invalid or does not exist for current service, we won't be able to revert auto OS patch settings to their system default value. [Service={0}]".format(str(self.current_auto_os_update_service))) @@ -773,103 +731,52 @@ def __remove_override_configuration_if_exists(self): self.composite_logger.log_debug("[DNF5] Override configuration file does not exist. Nothing to remove. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) return - self.composite_logger.log_debug("[DNF5] Removing override configuration file to restore machine default. [Path={0}]".format(self.os_patch_override_configuration_settings_file_path) ) - + self.composite_logger.log_debug("[DNF5] Removing override configuration file to restore machine default.[Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) code, out = self.env_layer.run_command_output(self.dnf5_automatic_remove_override_configuration_file_cmd, False, False) if code != 0: - error_msg = "[DNF5] Error removing override configuration file. [Command={0}][Code={1}][Output={2}]".format( - self.dnf5_automatic_remove_override_configuration_file_cmd, - str(code), - out - ) + error_msg = "[DNF5] Error removing override configuration file. [Command={0}][Code={1}][Output={2}]".format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out) self.composite_logger.log_error(error_msg) - self.status_handler.add_error_to_status( - error_msg, - Constants.PatchOperationErrorCodes.OPERATION_FAILED - ) + self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.OPERATION_FAILED) raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) - self.composite_logger.log_debug( - "[DNF5] Removed override configuration file. [Command={0}][Code={1}][Output={2}]" - .format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out) - ) + self.composite_logger.log_debug("[DNF5] Removed override configuration file. [Command={0}][Code={1}][Output={2}]".format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out)) def __restore_default_configuration_from_backup(self, default_backup): """Restore default dnf5-automatic configuration to its backed up state.""" - default_download_updates = default_backup[self.download_updates_identifier_text] default_apply_updates = default_backup[self.apply_updates_identifier_text] self.composite_logger.log_debug("[DNF5] Restoring default dnf5-automatic configuration values from backup.[Path={0}][download_updates={1}][apply_updates={2}]" - .format(self.os_patch_default_configuration_settings_file_path, str(default_download_updates),str(default_apply_updates) - ) - ) - - self.update_os_patch_configuration_sub_setting( - self.download_updates_identifier_text, - default_download_updates, - self.auto_update_config_pattern_match_text, - self.os_patch_default_configuration_settings_file_path - ) - - self.update_os_patch_configuration_sub_setting( - self.apply_updates_identifier_text, - default_apply_updates, - self.auto_update_config_pattern_match_text, - self.os_patch_default_configuration_settings_file_path - ) + .format(self.os_patch_default_configuration_settings_file_path, str(default_download_updates), str(default_apply_updates))) + + self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, default_download_updates, self.auto_update_config_pattern_match_text, self.os_patch_default_configuration_settings_file_path) + self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, default_apply_updates, self.auto_update_config_pattern_match_text, self.os_patch_default_configuration_settings_file_path) def __restore_override_configuration_from_backup(self, override_backup): """Restore override dnf5-automatic configuration to its backed up state.""" - override_download_updates = override_backup[self.download_updates_identifier_text] override_apply_updates = override_backup[self.apply_updates_identifier_text] # Empty values indicate override file did not exist before onboarding. if override_download_updates == "" and override_apply_updates == "": - self.composite_logger.log_debug( - "[DNF5] Override dnf5-automatic configuration did not exist before onboarding. " - "Removing override configuration file if it exists." - ) - + self.composite_logger.log_debug("[DNF5] Override dnf5-automatic configuration did not exist before onboarding.Removing override configuration file if it exists.") self.__remove_override_configuration_if_exists() return - self.composite_logger.log_debug( - "[DNF5] Restoring override dnf5-automatic configuration values from backup. " - "[Path={0}][download_updates={1}][apply_updates={2}]" - .format( - self.os_patch_override_configuration_settings_file_path, - str(override_download_updates), - str(override_apply_updates) - ) - ) + self.composite_logger.log_debug("[DNF5] Restoring override dnf5-automatic configuration values from backup. [Path={0}][download_updates={1}][apply_updates={2}]".format( + self.os_patch_override_configuration_settings_file_path, str(override_download_updates), str(override_apply_updates))) self.__ensure_override_configuration_exists() - self.update_os_patch_configuration_sub_setting( - self.download_updates_identifier_text, - override_download_updates, - self.auto_update_config_pattern_match_text, - self.os_patch_override_configuration_settings_file_path - ) - - self.update_os_patch_configuration_sub_setting( - self.apply_updates_identifier_text, - override_apply_updates, - self.auto_update_config_pattern_match_text, - self.os_patch_override_configuration_settings_file_path - ) + self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, override_download_updates, self.auto_update_config_pattern_match_text, self.os_patch_override_configuration_settings_file_path) + self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, override_apply_updates, self.auto_update_config_pattern_match_text, self.os_patch_override_configuration_settings_file_path) def __restore_enable_on_reboot_state_from_backup(self, default_backup): enable_on_reboot_value = default_backup[self.enable_on_reboot_identifier_text] if str(enable_on_reboot_value).lower() == 'true': - self.composite_logger.log_debug( - "[DNF5] Restoring dnf5-automatic timer to enabled state from backup." - ) - + self.composite_logger.log_debug("[DNF5] Restoring dnf5-automatic timer to enabled state from backup.") self.enable_auto_update_on_reboot() def enable_auto_update_on_reboot(self): @@ -885,7 +792,7 @@ def enable_auto_update_on_reboot(self): self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.OPERATION_FAILED) raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) else: - self.composite_logger.log_debug("[DNF5] Enabled auto update on reboot. [Command={0}][Code={1}][Output={2}]".format(command, str(code),out)) + self.composite_logger.log_debug("[DNF5] Enabled auto update on reboot.[Command={0}][Code={1}][Output={2}]".format(command, str(code), out)) def __get_image_default_patch_configuration_backup(self): """ Get image_default_patch_configuration_backup file""" diff --git a/src/core/tests/Test_Dnf5PackageManager.py b/src/core/tests/Test_Dnf5PackageManager.py index a3107dc8..122668a7 100644 --- a/src/core/tests/Test_Dnf5PackageManager.py +++ b/src/core/tests/Test_Dnf5PackageManager.py @@ -50,9 +50,12 @@ def mock_run_command_output_no_reboot(self, cmd, no_output=False, chk_err=True): "Reboot should not be necessary.\n") return 0, "" + def mock_run_command_output_remove_override_failure(self, cmd, no_output=False, chk_err=True): + return 1, "remove failed" + # region Utility Functions def __setup_config_and_invoke_revert_auto_os_to_system_default(self, package_manager, create_current_auto_os_config=True, create_backup_for_system_default_config=True, current_auto_os_update_config_value='', apply_updates_value="", - download_updates_value="", enable_on_reboot_value=False, installation_state_value=False, set_installation_state=True): + download_updates_value="", override_apply_updates_value="", override_download_updates_value="",enable_on_reboot_value=False, installation_state_value=False, set_installation_state=True): """ Sets up current auto OS update config, backup for system default config (if requested) and invoke revert to system default """ # setup current auto OS update config if create_current_auto_os_config: @@ -60,28 +63,42 @@ def __setup_config_and_invoke_revert_auto_os_to_system_default(self, package_man # setup backup for system default auto OS update config if create_backup_for_system_default_config: - self.__setup_backup_for_system_default_OS_update_config(package_manager, apply_updates_value=apply_updates_value, download_updates_value=download_updates_value, enable_on_reboot_value=enable_on_reboot_value, - installation_state_value=installation_state_value, set_installation_state=set_installation_state) + self.__setup_backup_for_system_default_OS_update_config(package_manager, apply_updates_value=apply_updates_value, download_updates_value=download_updates_value, + override_apply_updates_value = override_apply_updates_value, override_download_updates_value = override_download_updates_value, + enable_on_reboot_value=enable_on_reboot_value, installation_state_value=installation_state_value, set_installation_state=set_installation_state) package_manager.revert_auto_os_update_to_system_default() - def __setup_current_auto_os_update_config(self, package_manager, config_value='', - config_file_name="automatic.conf"): + def __setup_current_auto_os_update_config(self, package_manager, config_value='', config_file_name=""): # setup current auto OS update config - package_manager.dnf5_automatic_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, config_file_name) - self.runtime.write_to_file(package_manager.dnf5_automatic_configuration_file_path, config_value) + default_config_path = os.path.join(self.runtime.execution_config.config_folder, "default_automatic.conf") + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + + package_manager.dnf5_automatic_default_configuration_file_path = default_config_path + package_manager.dnf5_automatic_override_configuration_file_path = override_config_path - def __setup_backup_for_system_default_OS_update_config(self, package_manager, apply_updates_value="", download_updates_value="", enable_on_reboot_value=False, installation_state_value=False, set_installation_state=True): + self.runtime.write_to_file(default_config_path, config_value) + self.runtime.write_to_file(override_config_path, config_value) + + def __setup_backup_for_system_default_OS_update_config(self, package_manager, apply_updates_value="", download_updates_value="", override_apply_updates_value="", + override_download_updates_value="", enable_on_reboot_value=False, installation_state_value=False, set_installation_state=True): # setup backup for system default auto OS update config package_manager.image_default_patch_configuration_backup_path = os.path.join(self.runtime.execution_config.config_folder, Constants.IMAGE_DEFAULT_PATCH_CONFIGURATION_BACKUP_PATH) backup_image_default_patch_configuration_json = { - "dnf5-automatic": { - "apply_updates": apply_updates_value, - "download_updates": download_updates_value, - "enable_on_reboot": enable_on_reboot_value + package_manager.dnf5_default_auto_os_config_backup_key: { + package_manager.dnf5_automatic_apply_updates_identifier_text: apply_updates_value, + package_manager.dnf5_automatic_download_updates_identifier_text: download_updates_value, + package_manager.dnf5_automatic_enable_on_reboot_identifier_text: enable_on_reboot_value + }, + package_manager.dnf5_override_auto_os_config_backup_key: { + package_manager.dnf5_automatic_apply_updates_identifier_text: override_apply_updates_value, + package_manager.dnf5_automatic_download_updates_identifier_text: override_download_updates_value, + package_manager.dnf5_automatic_enable_on_reboot_identifier_text: enable_on_reboot_value } } + if set_installation_state: - backup_image_default_patch_configuration_json["dnf5-automatic"]["installation_state"] = installation_state_value + backup_image_default_patch_configuration_json[package_manager.dnf5_default_auto_os_config_backup_key][package_manager.dnf5_automatic_installation_state_identifier_text] = installation_state_value + backup_image_default_patch_configuration_json[package_manager.dnf5_override_auto_os_config_backup_key][package_manager.dnf5_automatic_installation_state_identifier_text] = installation_state_value self.runtime.write_to_file(package_manager.image_default_patch_configuration_backup_path, '{0}'.format(json.dumps(backup_image_default_patch_configuration_json))) @staticmethod @@ -97,12 +114,14 @@ def __assert_std_io(self, captured_output, expected_output=''): self.assertIn(expected_output, output) def __assert_reverted_automatic_patch_configuration_settings(self, package_manager, config_exists=True, config_value_expected=''): + reverted_dnf5_automatic_patch_configuration_settings = self.runtime.env_layer.file_system.read_with_retry( + package_manager.dnf5_automatic_default_configuration_file_path, raise_if_not_found=False) + if config_exists: - reverted_dnf5_automatic_patch_configuration_settings = self.runtime.env_layer.file_system.read_with_retry( - package_manager.dnf5_automatic_configuration_file_path) self.assertIsNotNone(reverted_dnf5_automatic_patch_configuration_settings) + self.assertEqual(config_value_expected, reverted_dnf5_automatic_patch_configuration_settings) else: - self.assertFalse(os.path.exists(package_manager.dnf5_automatic_configuration_file_path)) + self.assertIsNone(reverted_dnf5_automatic_patch_configuration_settings) # endregion def test_refresh_repo(self): @@ -125,30 +144,57 @@ def test_disable_auto_os_updates_with_uninstalled_services(self): # validating backup for dnf-automatic self.assertIn(package_manager.dnf5_default_auto_os_config_backup_key, image_default_patch_configuration_backup) - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_download_updates_identifier_text], "") - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_apply_updates_identifier_text], "") - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_installation_state_identifier_text], False) + self.assertIn(package_manager.dnf5_override_auto_os_config_backup_key, image_default_patch_configuration_backup) + + default_backup = image_default_patch_configuration_backup[package_manager.dnf5_default_auto_os_config_backup_key] + override_backup = image_default_patch_configuration_backup[package_manager.dnf5_override_auto_os_config_backup_key] + + self.assertEqual(default_backup[package_manager.dnf5_automatic_download_updates_identifier_text], "") + self.assertEqual(default_backup[package_manager.dnf5_automatic_apply_updates_identifier_text], "") + self.assertEqual(default_backup[package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) + self.assertEqual(default_backup[package_manager.dnf5_automatic_installation_state_identifier_text], False) + + self.assertEqual(override_backup[package_manager.dnf5_automatic_download_updates_identifier_text], "") + self.assertEqual(override_backup[package_manager.dnf5_automatic_apply_updates_identifier_text], "") + self.assertEqual(override_backup[package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) + self.assertEqual(override_backup[package_manager.dnf5_automatic_installation_state_identifier_text], False) def test_disable_auto_os_updates_with_installed_services(self): self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') - package_manager.dnf5_automatic_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") - dnf5_automatic_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' - self.runtime.write_to_file(package_manager.dnf5_automatic_configuration_file_path, dnf5_automatic_os_patch_configuration_settings) + default_config_path = os.path.join(self.runtime.execution_config.config_folder, "default_automatic.conf") + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + + dnf5_automatic_os_patch_configuration_settings = "apply_updates = yes\ndownload_updates = yes\n" + + package_manager.dnf5_automatic_default_configuration_file_path = default_config_path + package_manager.dnf5_automatic_override_configuration_file_path = override_config_path + + self.runtime.write_to_file(default_config_path, dnf5_automatic_os_patch_configuration_settings) + self.runtime.write_to_file(override_config_path, dnf5_automatic_os_patch_configuration_settings) package_manager.disable_auto_os_update() self.assertTrue(package_manager.image_default_patch_configuration_backup_exists()) + image_default_patch_configuration_backup = json.loads(self.runtime.env_layer.file_system.read_with_retry(package_manager.image_default_patch_configuration_backup_path)) - self.assertIsNot(image_default_patch_configuration_backup, None) - # validating backup for dnf-automatic - self.assertIn(package_manager.dnf5_auto_os_update_service, image_default_patch_configuration_backup) - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_download_updates_identifier_text], "yes") - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_apply_updates_identifier_text], "yes") - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) - self.assertEqual(image_default_patch_configuration_backup[package_manager.dnf5_auto_os_update_service][package_manager.dnf5_automatic_installation_state_identifier_text], True) + self.assertIsNotNone(image_default_patch_configuration_backup) + self.assertIn(package_manager.dnf5_default_auto_os_config_backup_key, image_default_patch_configuration_backup) + self.assertIn(package_manager.dnf5_override_auto_os_config_backup_key, image_default_patch_configuration_backup) + + default_backup = image_default_patch_configuration_backup[package_manager.dnf5_default_auto_os_config_backup_key] + override_backup = image_default_patch_configuration_backup[package_manager.dnf5_override_auto_os_config_backup_key] + + self.assertEqual(default_backup[package_manager.dnf5_automatic_download_updates_identifier_text], "yes") + self.assertEqual(default_backup[package_manager.dnf5_automatic_apply_updates_identifier_text], "yes") + self.assertEqual(default_backup[package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) + self.assertEqual(default_backup[package_manager.dnf5_automatic_installation_state_identifier_text], True) + + self.assertEqual(override_backup[package_manager.dnf5_automatic_download_updates_identifier_text], "yes") + self.assertEqual(override_backup[package_manager.dnf5_automatic_apply_updates_identifier_text], "yes") + self.assertEqual(override_backup[package_manager.dnf5_automatic_enable_on_reboot_identifier_text], False) + self.assertEqual(override_backup[package_manager.dnf5_automatic_installation_state_identifier_text], True) def test_disable_auto_os_update_failure(self): package_manager = self.container.get('package_manager') @@ -178,35 +224,45 @@ def test_get_current_auto_os_patch_state_with_uninstalled_services(self): self.assertEqual(current_auto_os_patch_state, Constants.AutomaticOSPatchStates.DISABLED) def test_get_current_auto_os_patch_state_with_installed_services_and_state_enabled(self): - # with enable on reboot set to false self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') package_manager.get_current_auto_os_patch_state = self.runtime.backup_get_current_auto_os_patch_state - package_manager.dnf5_automatic_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") - dnf5_automatic_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' - self.runtime.write_to_file(package_manager.dnf5_automatic_configuration_file_path, dnf5_automatic_os_patch_configuration_settings) + default_config_path = os.path.join(self.runtime.execution_config.config_folder, "default_automatic.conf") + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + + package_manager.dnf5_automatic_default_configuration_file_path = default_config_path + package_manager.dnf5_automatic_override_configuration_file_path = override_config_path + + self.runtime.write_to_file(default_config_path, "apply_updates = yes\ndownload_updates = yes\n") + self.runtime.write_to_file(override_config_path, "apply_updates = yes\ndownload_updates = yes\n") is_enabled = package_manager.is_service_set_to_enable_on_reboot(package_manager.enable_on_reboot_check_cmd) self.assertFalse(is_enabled) + current_auto_os_patch_state = package_manager.get_current_auto_os_patch_state() self.assertFalse(package_manager.image_default_patch_configuration_backup_exists()) self.assertEqual(current_auto_os_patch_state, Constants.AutomaticOSPatchStates.ENABLED) - # with enable on reboot set to true self.runtime.set_legacy_test_type('AnotherSadPath') package_manager = self.container.get('package_manager') package_manager.get_current_auto_os_patch_state = self.runtime.backup_get_current_auto_os_patch_state - package_manager.dnf5_automatic_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") - dnf5_automatic_os_patch_configuration_settings = 'apply_updates = no\ndownload_updates = yes\n' - self.runtime.write_to_file(package_manager.dnf5_automatic_configuration_file_path, - dnf5_automatic_os_patch_configuration_settings) + default_config_path = os.path.join(self.runtime.execution_config.config_folder, "default_automatic.conf") + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + + package_manager.dnf5_automatic_default_configuration_file_path = default_config_path + package_manager.dnf5_automatic_override_configuration_file_path = override_config_path + + self.runtime.write_to_file(default_config_path, "apply_updates = no\ndownload_updates = yes\n") + self.runtime.write_to_file(override_config_path, "apply_updates = no\ndownload_updates = yes\n") is_enabled = package_manager.is_service_set_to_enable_on_reboot(package_manager.enable_on_reboot_check_cmd) self.assertTrue(is_enabled) + current_auto_os_patch_state = package_manager.get_current_auto_os_patch_state() + self.assertFalse(package_manager.image_default_patch_configuration_backup_exists()) self.assertEqual(current_auto_os_patch_state, Constants.AutomaticOSPatchStates.ENABLED) @@ -233,8 +289,6 @@ def test_revert_auto_os_update_to_system_default_with_service_not_installed(self } } self.runtime.write_to_file(package_manager.image_default_patch_configuration_backup_path, json.dumps(backup_config)) - - # Should complete without error even when service is not installed package_manager.revert_auto_os_update_to_system_default() def test_revert_auto_os_update_to_system_default(self): @@ -311,7 +365,7 @@ def test_revert_auto_os_update_to_system_default(self): } }, "assertions": { - "config_value_expected": 'download_updates =\napply_updates = \n', + "config_value_expected": 'test_value = yes\ndownload_updates =\napply_updates = \n', "config_exists": True } } @@ -368,10 +422,39 @@ def test_revert_auto_os_update_to_system_default(self): } } + revert_success_with_override_backup_values_testcase = { + "legacy_type": "HappyPath", + "stdio": { + "capture_output": False, + "expected_output": None + }, + "config": { + "current_auto_update_config": { + "create_current_auto_os_config": True, + "current_auto_os_update_config_value": "apply_updates = no\ndownload_updates = no\n" + }, + "backup_system_default_config": { + "create_backup_for_system_default_config": True, + "apply_updates_value": "yes", + "download_updates_value": "yes", + "override_apply_updates_value": "yes", + "override_download_updates_value": "yes", + "enable_on_reboot_value": False, + "installation_state_value": True, + "set_installation_state": True + } + }, + "assertions": { + "config_exists": True, + "config_value_expected": "apply_updates = yes\ndownload_updates = yes\n" + } + } + all_testcases = [revert_success_testcase, revert_success_with_dnf_not_installed_testcase, revert_success_with_dnf_installed_but_no_config_value_testcase, revert_success_backup_config_does_not_exist_testcase, - revert_success_default_backup_config_invalid_testcase] + revert_success_default_backup_config_invalid_testcase, + revert_success_with_override_backup_values_testcase] for testcase in all_testcases: self.tearDown() @@ -391,6 +474,8 @@ def test_revert_auto_os_update_to_system_default(self): create_backup_for_system_default_config=bool(testcase["config"]["backup_system_default_config"]["create_backup_for_system_default_config"]), apply_updates_value=testcase["config"]["backup_system_default_config"]["apply_updates_value"], download_updates_value=testcase["config"]["backup_system_default_config"]["download_updates_value"], + override_apply_updates_value=testcase["config"]["backup_system_default_config"].get("override_apply_updates_value", ""), + override_download_updates_value=testcase["config"]["backup_system_default_config"].get("override_download_updates_value", ""), enable_on_reboot_value=bool(testcase["config"]["backup_system_default_config"]["enable_on_reboot_value"]), installation_state_value=bool(testcase["config"]["backup_system_default_config"]["installation_state_value"]), set_installation_state=bool(testcase["config"]["backup_system_default_config"]["set_installation_state"])) @@ -399,6 +484,7 @@ def test_revert_auto_os_update_to_system_default(self): # restore sys.stdout output sys.stdout = original_stdout self.__assert_std_io(captured_output=captured_output,expected_output=testcase["stdio"]["expected_output"]) + print("packagemanager" ,dir(package_manager)) 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_dedupe_update_packages_to_get_latest_versions(self): @@ -542,7 +628,7 @@ def test_package_manager(self): self.assertIsNotNone(package_manager) # Test: get_dependent_list - dependent_list = package_manager.get_dependent_list(["hyperv-daemons.x86_64"]) + dependent_list = package_manager.get_dependent_list(["openssl"]) self.assertIsNotNone(dependent_list) def test_install_package_success(self): @@ -583,45 +669,41 @@ def test_inclusion_type_other(self): def test_update_image_default_patch_mode(self): package_manager = self.container.get('package_manager') - package_manager.os_patch_configuration_settings_file_path = package_manager.dnf5_automatic_configuration_file_path = os.path.join( - self.runtime.execution_config.config_folder, "automatic.conf") + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + + package_manager.dnf5_automatic_override_configuration_file_path = override_config_path + package_manager.os_patch_override_configuration_settings_file_path = override_config_path + + package_manager.download_updates_identifier_text = package_manager.dnf5_automatic_download_updates_identifier_text + package_manager.apply_updates_identifier_text = package_manager.dnf5_automatic_apply_updates_identifier_text + package_manager.auto_update_config_pattern_match_text = package_manager.dnf5_automatic_config_pattern_match_text - # disable apply_updates when enabled by default dnf5_automatic_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' - self.runtime.write_to_file(package_manager.dnf5_automatic_configuration_file_path, - dnf5_automatic_os_patch_configuration_settings) - - package_manager.update_os_patch_configuration_sub_setting( - package_manager.dnf5_automatic_apply_updates_identifier_text, "no", - package_manager.dnf5_automatic_config_pattern_match_text) - dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry( - package_manager.os_patch_configuration_settings_file_path) + self.runtime.write_to_file(override_config_path, dnf5_automatic_os_patch_configuration_settings) + + package_manager.update_os_patch_configuration_sub_setting(package_manager.dnf5_automatic_apply_updates_identifier_text, "no", package_manager.dnf5_automatic_config_pattern_match_text) + dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry(override_config_path) + self.assertIsNotNone(dnf5_automatic_os_patch_configuration_settings_file_path_read) self.assertIn('apply_updates = no', dnf5_automatic_os_patch_configuration_settings_file_path_read) - self.assertIn('download_updates = yes' , dnf5_automatic_os_patch_configuration_settings_file_path_read) + self.assertIn('download_updates = yes', dnf5_automatic_os_patch_configuration_settings_file_path_read) - # disable download_updates when enabled by default dnf5_automatic_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' - self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, - dnf5_automatic_os_patch_configuration_settings) - package_manager.update_os_patch_configuration_sub_setting( - package_manager.dnf5_automatic_download_updates_identifier_text, "no", - package_manager.dnf5_automatic_config_pattern_match_text) - dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry( - package_manager.os_patch_configuration_settings_file_path) + self.runtime.write_to_file(override_config_path, dnf5_automatic_os_patch_configuration_settings) + + package_manager.update_os_patch_configuration_sub_setting(package_manager.dnf5_automatic_download_updates_identifier_text, "no", package_manager.dnf5_automatic_config_pattern_match_text) + dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry(override_config_path) + self.assertIsNotNone(dnf5_automatic_os_patch_configuration_settings_file_path_read) self.assertIn('apply_updates = yes', dnf5_automatic_os_patch_configuration_settings_file_path_read) self.assertIn('download_updates = no', dnf5_automatic_os_patch_configuration_settings_file_path_read) - # disable apply_updates when default patch mode settings file is empty dnf5_automatic_os_patch_configuration_settings = '' - self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, - dnf5_automatic_os_patch_configuration_settings) - package_manager.update_os_patch_configuration_sub_setting( - package_manager.dnf5_automatic_apply_updates_identifier_text, "no", - package_manager.dnf5_automatic_config_pattern_match_text) - dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry( - package_manager.os_patch_configuration_settings_file_path) + self.runtime.write_to_file(override_config_path, dnf5_automatic_os_patch_configuration_settings) + + package_manager.update_os_patch_configuration_sub_setting(package_manager.dnf5_automatic_apply_updates_identifier_text, "no", package_manager.dnf5_automatic_config_pattern_match_text) + dnf5_automatic_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry(override_config_path) + self.assertIsNotNone(dnf5_automatic_os_patch_configuration_settings_file_path_read) self.assertNotIn('download_updates', dnf5_automatic_os_patch_configuration_settings_file_path_read) self.assertIn('apply_updates = no', dnf5_automatic_os_patch_configuration_settings_file_path_read) @@ -723,6 +805,15 @@ def test_no_op_methods(self): package_manager.set_security_esm_package_status("op", []) package_manager.separate_out_esm_packages([], []) + def test_remove_override_configuration_failure(self): + self.runtime.set_legacy_test_type('HappyPath') + package_manager = self.container.get('package_manager') + override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + package_manager.os_patch_override_configuration_settings_file_path = override_config_path + self.runtime.write_to_file(override_config_path, "apply_updates = yes\ndownload_updates = yes\n") + self.runtime.env_layer.run_command_output = self.mock_run_command_output_remove_override_failure + self.assertRaises(Exception, package_manager._Dnf5PackageManager__remove_override_configuration_if_exists) + if __name__ == '__main__': unittest.main() diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index fb0fa6ce..2f715be4 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -1091,6 +1091,22 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): output = ( "Installed packages\n" "rubygem-json.x86_64 2.13.2-2.azl4~20260501 azurelinux-base\n") + elif cmd.find("sudo dnf5 install --assumeno --skip-broken openssl") > -1: + code = 0 + output = "Updating and loading repositories:\n" + \ + "Repositories loaded.\n" + \ + "Package Arch Version Repository Size\n" + \ + "Upgrading:\n" + \ + " openssl x86_64 1:3.5.4-7.azl4 azurelinux-base 1.8 MiB\n" + \ + " replacing openssl x86_64 1:3.5.4-4.azl4 64806a6b30824b51aafe6d2d87587286 1.8 MiB\n" + \ + " openssl-libs x86_64 1:3.5.4-7.azl4 azurelinux-base 6.5 MiB\n" + \ + " replacing openssl-libs x86_64 1:3.5.4-4.azl4 64806a6b30824b51aafe6d2d87587286 6.5 MiB\n" + \ + "Transaction Summary:\n" + \ + " Upgrading: 2 packages\n" + \ + " Replacing: 2 packages\n\n" + \ + "Total size of inbound packages is 3 MiB. Need to download 3 MiB.\n" + \ + "After this operation, 32 B extra will be used (install 8 MiB, remove 8 MiB).\n" + \ + "Operation aborted by the user." elif self.legacy_test_type == 'FailInstallPath': if cmd.find("cat /proc/cpuinfo | grep name") > -1: code = 0 From 64846d3bc956d780f680ab7bcc2248bdd766f56e Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Thu, 6 Aug 2026 10:38:31 -0400 Subject: [PATCH 4/6] Address code review + AI comments --- .../package_managers/Dnf5PackageManager.py | 30 +++++++++---------- src/core/tests/Test_Dnf5PackageManager.py | 2 +- .../tests/library/LegacyEnvLayerExtensions.py | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index c98b4fe4..adac9fb9 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -304,7 +304,7 @@ def extract_dependencies(self, output, packages): # Remove input packages (support both pkg and pkg.arch) if len(dependent_package_name) != 0 and dependent_package_name not in packages and dependent_package_name not in dependencies: - self.composite_logger.log_debug("[DNF5] > Dependency detected: " + dependent_package_name) + self.composite_logger.log_verbose("[DNF5] > Dependency detected: " + dependent_package_name) dependencies.append(dependent_package_name) return dependencies @@ -365,7 +365,6 @@ def __init_constants_for_dnf5_automatic(self): self.dnf5_auto_os_update_service = "dnf5-automatic" self.dnf5_default_auto_os_config_backup_key = "default-dnf5-automatic" self.dnf5_override_auto_os_config_backup_key = "override-dnf5-automatic" - self.dnf5_automatic_remove_override_configuration_file_cmd = 'rm -f /etc/dnf/automatic.conf' def get_current_auto_os_patch_state(self): """ Gets the current auto OS update patch state on the machine """ @@ -557,7 +556,7 @@ def backup_image_default_patch_configuration_if_not_exists(self): is_backup_valid = self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup) if is_backup_valid: - self.composite_logger.log_debug("[DNF5] Since extension has a valid backup, no need to log the current settings again.[Default Auto OS update settings={0}] [File path={1}]".format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path)) + self.composite_logger.log_debug("[DNF5] Since extension has a valid backup, no need to log the current settings again. [Default Auto OS update settings={0}] [File path={1}]".format(str(image_default_patch_configuration_backup), self.image_default_patch_configuration_backup_path)) else: self.composite_logger.log_debug("[DNF5] Since the backup is invalid, will add a new backup with the current auto OS update settings") self.composite_logger.log_verbose("[DNF5] Fetching current auto OS update settings for [AutoOSUpdateService={0}]".format(str(self.current_auto_os_update_service))) @@ -630,15 +629,15 @@ def is_image_default_patch_configuration_backup_valid(self, image_default_patch_ return self.is_backup_valid_for_dnf5_automatic(image_default_patch_configuration_backup) def is_backup_valid_for_dnf5_automatic(self, image_default_patch_configuration_backup): - default_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_default_configuration_backup_key) - override_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_override_configuration_backup_key) + default_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_default_configuration_backup_key) + override_backup_valid = self.__is_backup_valid(image_default_patch_configuration_backup, self.os_patch_override_configuration_backup_key) - if default_backup_valid and override_backup_valid: - self.composite_logger.log_debug("[DNF5] Extension has a valid backup for default and override dnf5-automatic configuration settings") - return True + if default_backup_valid and override_backup_valid: + self.composite_logger.log_debug("[DNF5] Extension has a valid backup for default and override dnf5-automatic configuration settings") + return True - self.composite_logger.log_debug("[DNF5] Extension does not have a valid backup for default and override dnf5-automatic configuration settings") - return False + self.composite_logger.log_debug("[DNF5] Extension does not have a valid backup for default and override dnf5-automatic configuration settings") + return False def __is_backup_valid(self, image_default_patch_configuration_backup, backup_key): return (backup_key in image_default_patch_configuration_backup @@ -717,8 +716,8 @@ def revert_auto_os_update_to_system_default_for_dnf5_automatic(self): self.composite_logger.log_debug("[DNF5] Since the backup is invalid or does not exist for current service, we won't be able to revert auto OS patch settings to their system default value. [Service={0}]".format(str(self.current_auto_os_update_service))) def __remove_override_configuration_if_exists(self): - """Removes dnf5-automatic override configuration file if it exists.Missing override file is valid by design, so this method must not throw - when the file is absent.""" + """Removes dnf5-automatic override configuration file if it exists. Missing override file is valid by design, so this method must not throw + when the file is absent.""" override_config_file = self.env_layer.file_system.read_with_retry(self.os_patch_override_configuration_settings_file_path, raise_if_not_found=False) if override_config_file is None: @@ -726,15 +725,16 @@ def __remove_override_configuration_if_exists(self): return self.composite_logger.log_debug("[DNF5] Removing override configuration file to restore machine default.[Path={0}]".format(self.os_patch_override_configuration_settings_file_path)) - code, out = self.env_layer.run_command_output(self.dnf5_automatic_remove_override_configuration_file_cmd, False, False) + command = "rm -f {0}".format(self.os_patch_override_configuration_settings_file_path) + code, out = self.env_layer.run_command_output(command, False, False) if code != 0: - error_msg = "[DNF5] Error removing override configuration file. [Command={0}][Code={1}][Output={2}]".format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out) + error_msg = "[DNF5] Error removing override configuration file. [Command={0}][Code={1}][Output={2}]".format(command, str(code), out) self.composite_logger.log_error(error_msg) self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.OPERATION_FAILED) raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) - self.composite_logger.log_debug("[DNF5] Removed override configuration file. [Command={0}][Code={1}][Output={2}]".format(self.dnf5_automatic_remove_override_configuration_file_cmd, str(code), out)) + self.composite_logger.log_debug("[DNF5] Removed override configuration file. [Command={0}][Code={1}][Output={2}]".format(command, str(code), out)) def __restore_default_configuration_from_backup(self, default_backup): """Restore default dnf5-automatic configuration to its backed up state.""" diff --git a/src/core/tests/Test_Dnf5PackageManager.py b/src/core/tests/Test_Dnf5PackageManager.py index 3847b160..2247333a 100644 --- a/src/core/tests/Test_Dnf5PackageManager.py +++ b/src/core/tests/Test_Dnf5PackageManager.py @@ -68,7 +68,7 @@ def __setup_config_and_invoke_revert_auto_os_to_system_default(self, package_man enable_on_reboot_value=enable_on_reboot_value, installation_state_value=installation_state_value, set_installation_state=set_installation_state) package_manager.revert_auto_os_update_to_system_default() - def __setup_current_auto_os_update_config(self, package_manager, config_value='', config_file_name=""): + def __setup_current_auto_os_update_config(self, package_manager, config_value=''): # setup current auto OS update config default_config_path = os.path.join(self.runtime.execution_config.config_folder, "default_automatic.conf") override_config_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index ac491070..5fffcce9 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -1117,7 +1117,7 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): output = ( "Installed packages\n" "rubygem-json.x86_64 2.13.2-2.azl4~20260501 azurelinux-base\n") - elif cmd.find("sudo dnf5 install --assumeno --skip-broken openssl") > -1: + elif cmd.find("sudo dnf5 upgrade --assumeno openssl") > -1: code = 0 output = "Updating and loading repositories:\n" + \ "Repositories loaded.\n" + \ From 2eeb7353680d1b28f1c8ebae763414b428130a73 Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Mon, 10 Aug 2026 14:27:38 -0400 Subject: [PATCH 5/6] - Remove unused mock --- src/core/tests/library/LegacyEnvLayerExtensions.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index 5fffcce9..d52de5da 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -1098,20 +1098,6 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): 'Repositories loaded.\n' 'Package "rubygem-json-2.13.2-2.azl4~20260501.x86_64" is already installed.\n\n' 'Nothing to do.\n') - elif "hyperv-daemons" in cmd and "--assumeno" in cmd: - code = 1 - output = ( - "Updating and loading repositories:\n" - "Repositories loaded.\n" - "Package Arch Version Repository Size\n" - "Installing:\n" - " hyperv-daemons x86_64 6.10-3.azl4~20260501 azurelinux-base 20.08k\n\n" - "Installing dependencies:\n" - " hyperv-daemons-license noarch 6.10-3.azl4~20260501 azurelinux-base 18.3 KiB\n\n" - "Transaction Summary:\n" - " Installing: 2 packages\n\n" - "Total download size: 135.09k\n" - "Operation aborted by the user.\n") elif "dnf5 list --installed rubygem-json" in cmd: code = 0 output = ( From af9cf8089236c6948d8d2d94a87abdc83226e9ac Mon Sep 17 00:00:00 2001 From: Yashna Parikh Date: Mon, 10 Aug 2026 15:38:50 -0400 Subject: [PATCH 6/6] Address code review + coverage --- .../package_managers/Dnf5PackageManager.py | 4 ++-- src/core/tests/Test_Dnf5PackageManager.py | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/core/src/package_managers/Dnf5PackageManager.py b/src/core/src/package_managers/Dnf5PackageManager.py index adac9fb9..fb9b0896 100644 --- a/src/core/src/package_managers/Dnf5PackageManager.py +++ b/src/core/src/package_managers/Dnf5PackageManager.py @@ -754,7 +754,7 @@ def __restore_override_configuration_from_backup(self, override_backup): # Empty values indicate override file did not exist before onboarding. if override_download_updates == "" and override_apply_updates == "": - self.composite_logger.log_debug("[DNF5] Override dnf5-automatic configuration did not exist before onboarding.Removing override configuration file if it exists.") + self.composite_logger.log_debug("[DNF5] Override dnf5-automatic configuration did not exist before onboarding. Removing override configuration file if it exists.") self.__remove_override_configuration_if_exists() return @@ -786,7 +786,7 @@ def enable_auto_update_on_reboot(self): self.status_handler.add_error_to_status(error_msg, Constants.PatchOperationErrorCodes.OPERATION_FAILED) raise Exception(error_msg, "[{0}]".format(Constants.ERROR_ADDED_TO_STATUS)) else: - self.composite_logger.log_debug("[DNF5] Enabled auto update on reboot.[Command={0}][Code={1}][Output={2}]".format(command, str(code), out)) + self.composite_logger.log_debug("[DNF5] Enabled auto update on reboot. [Command={0}][Code={1}][Output={2}]".format(command, str(code), out)) def __get_image_default_patch_configuration_backup(self): """ Get image_default_patch_configuration_backup file""" diff --git a/src/core/tests/Test_Dnf5PackageManager.py b/src/core/tests/Test_Dnf5PackageManager.py index 2247333a..a03843fd 100644 --- a/src/core/tests/Test_Dnf5PackageManager.py +++ b/src/core/tests/Test_Dnf5PackageManager.py @@ -42,7 +42,7 @@ def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode= def mock_run_command_output_check_update(self, cmd, no_output=False, chk_err=True): if "check-update" in cmd: return 0, "" - return None + return 0, "" def mock_run_command_output_no_reboot(self, cmd, no_output=False, chk_err=True): if "needs-restarting" in cmd: @@ -484,7 +484,6 @@ def test_revert_auto_os_update_to_system_default(self): # restore sys.stdout output sys.stdout = original_stdout self.__assert_std_io(captured_output=captured_output,expected_output=testcase["stdio"]["expected_output"]) - print("packagemanager" ,dir(package_manager)) 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_dedupe_update_packages_to_get_latest_versions(self): @@ -746,13 +745,19 @@ def test_is_reboot_pending(self): # Restart not required (needs-restarting returns code=0) self.runtime.set_legacy_test_type('SadPath') - self.runtime.env_layer.run_output_command = self.mock_run_command_output_no_reboot + self.runtime.env_layer.run_command_output = self.mock_run_command_output_no_reboot self.assertFalse(package_manager.is_reboot_pending()) # Exception Path self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') self.assertIsNotNone(package_manager) + package_manager.os_patch_override_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "override.conf") + self.runtime.write_to_file(package_manager.os_patch_override_configuration_settings_file_path, '[commands]\napply_updates = yes\ndownload_updates = yes\n') + + self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception + self.assertRaises(Exception, package_manager.update_os_patch_configuration_sub_setting, package_manager.download_updates_identifier_text, "no", + package_manager.auto_update_config_pattern_match_text) self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception self.assertRaises(Exception, package_manager.is_reboot_pending()) @@ -784,6 +789,10 @@ def test_update_os_patch_configuration_sub_setting_exception_handling(self): self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') # Mock file_system.write_with_retry to raise exception + package_manager.os_patch_override_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "override.conf") + self.runtime.write_to_file(package_manager.os_patch_override_configuration_settings_file_path, '[commands]\napply_updates = yes\ndownload_updates = yes\n') + self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception + self.assertRaises(Exception, package_manager.update_os_patch_configuration_sub_setting) self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception self.assertRaises(Exception, package_manager.update_os_patch_configuration_sub_setting, ) @@ -792,6 +801,11 @@ def test_backup_image_default_patch_configuration_if_not_exists_exception_handli self.runtime.set_legacy_test_type('HappyPath') package_manager = self.container.get('package_manager') # Mock file_system.write_with_retry to raise exception + package_manager.os_patch_override_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "override.conf") + self.runtime.write_to_file(package_manager.os_patch_override_configuration_settings_file_path, '[commands]\napply_updates = yes\ndownload_updates = yes\n') + + self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception + self.assertRaises(Exception, package_manager.update_os_patch_configuration_sub_setting) self.runtime.env_layer.file_system.write_with_retry = self.mock_write_with_retry_raise_exception self.assertRaises(Exception, package_manager.backup_image_default_patch_configuration_if_not_exists, )