From aaa60bcb59f7a282ac8337a1cf31e9e120bb1e08 Mon Sep 17 00:00:00 2001 From: Rajasi Rane <56841542+rane-rajasi@users.noreply.github.com> Date: Fri, 24 Sep 2021 17:09:03 -0700 Subject: [PATCH] Disable auto OS updates in YUM (#93) * Disabling auto OS updates in yum * minor corrections * Addressing review comments * Resolving bugs in diable auto OS updates in yum and addresing some feedback comments * Merging with master and adressing some more review comments * Adding UTs and addressing some more PR comments * updating extension version to 1.6.27 --- src/core/src/bootstrap/Constants.py | 9 +- src/core/src/bootstrap/EnvLayer.py | 24 +- .../core_logic/ConfigurePatchingProcessor.py | 11 +- .../AptitudePackageManager.py | 28 +- .../src/package_managers/PackageManager.py | 17 +- .../src/package_managers/YumPackageManager.py | 471 +++++++++++++++++- src/core/tests/Test_AptitudePackageManager.py | 42 +- src/core/tests/Test_YumPackageManager.py | 126 ++++- .../tests/library/LegacyEnvLayerExtensions.py | 12 + src/extension/src/Constants.py | 2 +- .../file_handlers/ExtOutputStatusHandler.py | 2 +- src/extension/src/manifest.xml | 2 +- 12 files changed, 683 insertions(+), 63 deletions(-) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 123a430b..80919a82 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -30,7 +30,7 @@ def __iter__(self): UNKNOWN = "Unknown" # Extension version (todo: move to a different file) - EXT_VERSION = "1.6.26" + EXT_VERSION = "1.6.27" # Runtime environments TEST = 'Test' @@ -112,6 +112,13 @@ class AutomaticOSPatchStates(EnumBackport): DISABLED = "Disabled" ENABLED = "Enabled" + # List of auto OS update services in Yum + # todo: move to yumpackagemanager + class YumAutoOSUpdateServices(EnumBackport): + YUM_CRON = "yum-cron" + DNF_AUTOMATIC = "dnf-automatic" + PACKAGEKIT = "packagekit" + # auto assessment states class AutoAssessmentStates(EnumBackport): UNKNOWN = "Unknown" diff --git a/src/core/src/bootstrap/EnvLayer.py b/src/core/src/bootstrap/EnvLayer.py index e5c433df..576bf516 100644 --- a/src/core/src/bootstrap/EnvLayer.py +++ b/src/core/src/bootstrap/EnvLayer.py @@ -274,7 +274,7 @@ def resolve_path(self, requested_path): else: return requested_path - def open(self, file_path, mode): + def open(self, file_path, mode, raise_if_not_found=True): """ Provides a file handle to the file_path requested using implicit redirection where required """ real_path = self.resolve_path(file_path) for i in range(0, Constants.MAX_FILE_OPERATION_RETRY_COUNT): @@ -284,24 +284,29 @@ def open(self, file_path, mode): if i < Constants.MAX_FILE_OPERATION_RETRY_COUNT: time.sleep(i + 1) else: - raise Exception("Unable to open {0} (retries exhausted). Error: {1}.".format(str(real_path), repr(error))) + error_message = "Unable to open file (retries exhausted). [File={0}][Error={1}][RaiseIfNotFound={2}].".format(str(real_path), repr(error), str(raise_if_not_found)) + if raise_if_not_found: + raise Exception(error_message) + else: + print(error_message) + return None - def __obtain_file_handle(self, file_path_or_handle, mode='a+'): + def __obtain_file_handle(self, file_path_or_handle, mode='a+', raise_if_not_found=True): """ Pass-through for handle. For path, resolution and handle open with retry. """ is_path = False if isinstance(file_path_or_handle, str) or not(hasattr(file_path_or_handle, 'read') and hasattr(file_path_or_handle, 'write')): is_path = True - file_path_or_handle = self.open(file_path_or_handle, mode) + file_path_or_handle = self.open(file_path_or_handle, mode, raise_if_not_found) file_handle = file_path_or_handle return file_handle, is_path - def read_with_retry(self, file_path_or_handle): + def read_with_retry(self, file_path_or_handle, raise_if_not_found=True): """ Reads all content from a given file path in a single operation """ operation = "FILE_READ" # only fully emulate non_exclusive_files from the real recording; exclusive files can be redirected and handled in emulator scenarios if not self.__emulator_enabled or (isinstance(file_path_or_handle, str) and os.path.basename(file_path_or_handle) not in self.__non_exclusive_files): - file_handle, was_path = self.__obtain_file_handle(file_path_or_handle, 'r') + file_handle, was_path = self.__obtain_file_handle(file_path_or_handle, 'r', raise_if_not_found) for i in range(0, Constants.MAX_FILE_OPERATION_RETRY_COUNT): try: value = file_handle.read() @@ -313,7 +318,12 @@ def read_with_retry(self, file_path_or_handle): if i < Constants.MAX_FILE_OPERATION_RETRY_COUNT: time.sleep(i + 1) else: - raise Exception("Unable to read from {0} (retries exhausted). Error: {1}.".format(str(file_path_or_handle), repr(error))) + error_message = "Unable to read file (retries exhausted). [File={0}][Error={1}][RaiseIfNotFound={2}].".format(str(file_path_or_handle), repr(error), str(raise_if_not_found)) + if raise_if_not_found: + raise Exception(error_message) + else: + print(error_message) + return None else: code, output = self.__read_record(operation) return output diff --git a/src/core/src/core_logic/ConfigurePatchingProcessor.py b/src/core/src/core_logic/ConfigurePatchingProcessor.py index e638ebf9..c82d0298 100644 --- a/src/core/src/core_logic/ConfigurePatchingProcessor.py +++ b/src/core/src/core_logic/ConfigurePatchingProcessor.py @@ -65,17 +65,20 @@ def __try_set_patch_mode(self): try: self.status_handler.set_current_operation(Constants.CONFIGURE_PATCHING) self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state() - self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(self.current_auto_os_patch_state))) # disable auto OS updates if VM is configured for platform updates only. # NOTE: this condition will be false for Assessment operations, since patchMode is not sent in the API request - if self.current_auto_os_patch_state == Constants.AutomaticOSPatchStates.ENABLED and self.execution_config.patch_mode == Constants.PatchModes.AUTOMATIC_BY_PLATFORM: + if self.current_auto_os_patch_state != Constants.AutomaticOSPatchStates.DISABLED and self.execution_config.patch_mode == Constants.PatchModes.AUTOMATIC_BY_PLATFORM: self.package_manager.disable_auto_os_update() self.current_auto_os_patch_state = self.package_manager.get_current_auto_os_patch_state() - self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(self.current_auto_os_patch_state))) - self.__report_consolidated_configure_patch_status() + if self.current_auto_os_patch_state == Constants.AutomaticOSPatchStates.UNKNOWN: + # NOTE: only sending details in error objects for customer visibility on why patch state is unknown, overall configurepatching status will remain successful + self.__report_consolidated_configure_patch_status(error="Extension attempted but could not disable some of the auto OS update service. Please check if the auto OS services are configured correctly") + else: + self.__report_consolidated_configure_patch_status() + self.composite_logger.log_debug("Completed processing patch mode configuration.") except Exception as error: self.composite_logger.log_error("Error while processing patch mode configuration. [Error={0}]".format(repr(error))) diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index 3afdcb20..61a87c18 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -395,11 +395,14 @@ def get_current_auto_os_patch_state(self): self.composite_logger.log("Fetching the current automatic OS patch state on the machine...") self.__get_current_auto_os_updates_setting_on_machine() if int(self.unattended_upgrade_value) == 0: - return Constants.AutomaticOSPatchStates.DISABLED + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.DISABLED elif int(self.unattended_upgrade_value) == 1: - return Constants.AutomaticOSPatchStates.ENABLED + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.ENABLED else: - return Constants.AutomaticOSPatchStates.UNKNOWN + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.UNKNOWN + + self.composite_logger.log_debug("Current Auto OS Patch State is [State={0}]".format(str(current_auto_os_patch_state))) + return current_auto_os_patch_state def __get_current_auto_os_updates_setting_on_machine(self): """ Gets all the update settings related to auto OS updates currently set on the machine """ @@ -437,7 +440,22 @@ def backup_image_default_patch_configuration_if_not_exists(self): """ Records the default system settings for auto OS updates within patch extension artifacts for future reference. We only log the default system settings a VM comes with, any subsequent updates will not be recorded""" try: - if not self.image_default_patch_configuration_backup_exists(): + image_default_patch_configuration_backup = {} + + # read existing backup since it also contains backup from other update services. We need to preserve any existing data with backup file + if self.image_default_patch_configuration_backup_exists(): + try: + image_default_patch_configuration_backup = json.loads(self.env_layer.file_system.read_with_retry(self.image_default_patch_configuration_backup_path)) + except Exception as error: + self.composite_logger.log_error("Unable to read backup for default patch state. Will attempt to re-write. [Exception={0}]".format(repr(error))) + + # 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("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("Since the backup is invalid, will add a new backup with the current auto OS update settings") self.__get_current_auto_os_updates_setting_on_machine() backup_image_default_patch_configuration_json = { @@ -462,7 +480,7 @@ def is_image_default_patch_configuration_backup_valid(self, image_default_patch_ self.composite_logger.log_error("Extension does not have a valid backup of the default system configuration settings for auto OS updates.") return False - def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="0"): + def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="0", patch_configuration_sub_setting_pattern_to_match=""): """ Updates (or adds if it doesn't exist) the given patch_configuration_sub_setting with the given value in os_patch_configuration_settings_file """ 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 diff --git a/src/core/src/package_managers/PackageManager.py b/src/core/src/package_managers/PackageManager.py index 4420b9e0..89aaaff4 100644 --- a/src/core/src/package_managers/PackageManager.py +++ b/src/core/src/package_managers/PackageManager.py @@ -348,27 +348,14 @@ def image_default_patch_configuration_backup_exists(self): self.composite_logger.log_debug("Default system configuration settings for auto OS updates aren't recorded in the extension") return False - # verify if the existing backup is valid - try: - image_default_patch_configuration_backup = json.loads(self.env_layer.file_system.read_with_retry(self.image_default_patch_configuration_backup_path)) - if self.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup): - self.composite_logger.log_debug("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)) - return True - else: - self.composite_logger.log_error("Since the backup is invalid, will add a new backup with the current auto OS update settings") - return False - except Exception as error: - self.composite_logger.log_error("Unable to read backup for default auto OS update settings. [Exception={0}]".format(repr(error))) - return False + return True @abstractmethod def is_image_default_patch_configuration_backup_valid(self, image_default_patch_configuration_backup): pass @abstractmethod - def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value): + def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value, patch_configuration_sub_setting_pattern_to_match): pass # endregion diff --git a/src/core/src/package_managers/YumPackageManager.py b/src/core/src/package_managers/YumPackageManager.py index aaf09a8b..6f4cbaef 100644 --- a/src/core/src/package_managers/YumPackageManager.py +++ b/src/core/src/package_managers/YumPackageManager.py @@ -15,6 +15,7 @@ # Requires Python 2.7+ """YumPackageManager for Redhat and CentOS""" +import json import re from core.src.package_managers.PackageManager import PackageManager from core.src.bootstrap.Constants import Constants @@ -52,6 +53,30 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.yum_ps_prerequisite = 'sudo yum -y install yum-plugin-ps' self.yum_ps = 'sudo yum ps' + # auto OS updates + self.current_auto_os_update_service = None + self.os_patch_configuration_settings_file_path = '' + self.auto_update_service_enabled = False + self.auto_update_config_pattern_match_text = "" + self.download_updates_identifier_text = "" + self.apply_updates_identifier_text = "" + self.enable_on_reboot_identifier_text = "" + self.enable_on_reboot_check_cmd = '' + self.installation_state_identifier_text = "" + self.install_check_cmd = "" + self.apply_updates_enabled = "Enabled" + self.apply_updates_disabled = "Disabled" + self.apply_updates_unknown = "Unknown" + + # commands for YUM Cron service + self.__init_constants_for_yum_cron() + + # commands for DNF Automatic updates service + self.__init_constants_for_dnf_automatic() + + # commands for PackageKit service + self.__init_constants_for_packagekit() + # Miscellaneous self.set_package_manager_setting(Constants.PKG_MGR_SETTING_IDENTITY, Constants.YUM) self.STR_TOTAL_DOWNLOAD_SIZE = "Total download size: " @@ -345,28 +370,448 @@ def get_package_size(self, output): # endregion # region auto OS updates + def __init_constants_for_yum_cron(self): + self.yum_cron_configuration_settings_file_path = '/etc/yum/yum-cron.conf' + self.yum_cron_install_check_cmd = 'systemctl list-unit-files --type=service | grep yum-cron.service' # list-unit-files returns installed services, ref: https://www.freedesktop.org/software/systemd/man/systemctl.html#Unit%20File%20Commands + self.yum_cron_enable_on_reboot_check_cmd = 'systemctl is-enabled yum-cron' + self.yum_cron_disable_on_reboot_cmd = 'systemctl disable yum-cron' + self.yum_cron_config_pattern_match_text = ' = (no|yes)' + self.yum_cron_download_updates_identifier_text = 'download_updates' + self.yum_cron_apply_updates_identifier_text = 'apply_updates' + self.yum_cron_enable_on_reboot_identifier_text = "enable_on_reboot" + self.yum_cron_installation_state_identifier_text = "installation_state" + + def __init_constants_for_dnf_automatic(self): + self.dnf_automatic_configuration_file_path = '/etc/dnf/automatic.conf' + self.dnf_automatic_install_check_cmd = 'systemctl list-unit-files --type=service | grep dnf-automatic.service' # list-unit-files returns installed services, ref: https://www.freedesktop.org/software/systemd/man/systemctl.html#Unit%20File%20Commands + self.dnf_automatic_enable_on_reboot_check_cmd = 'systemctl is-enabled dnf-automatic.timer' + self.dnf_automatic_disable_on_reboot_cmd = 'systemctl disable dnf-automatic.timer' + self.dnf_automatic_config_pattern_match_text = ' = (no|yes)' + self.dnf_automatic_download_updates_identifier_text = 'download_updates' + self.dnf_automatic_apply_updates_identifier_text = 'apply_updates' + self.dnf_automatic_enable_on_reboot_identifier_text = "enable_on_reboot" + self.dnf_automatic_installation_state_identifier_text = "installation_state" + + def __init_constants_for_packagekit(self): + self.packagekit_configuration_file_path = '/etc/PackageKit/PackageKit.conf' + self.packagekit_install_check_cmd = 'systemctl list-unit-files --type=service | grep packagekit.service' # list-unit-files returns installed services, ref: https://www.freedesktop.org/software/systemd/man/systemctl.html#Unit%20File%20Commands + self.packagekit_enable_on_reboot_check_cmd = 'systemctl is-enabled packagekit' + self.packagekit_disable_on_reboot_cmd = 'systemctl disable packagekit' + self.packagekit_config_pattern_match_text = ' = (false|true)' + self.packagekit_download_updates_identifier_text = 'GetPreparedUpdates' # todo: dummy value, get real value or add telemetry to gather value + self.packagekit_apply_updates_identifier_text = 'WritePreparedUpdates' + self.packagekit_enable_on_reboot_identifier_text = "enable_on_reboot" + self.packagekit_installation_state_identifier_text = "installation_state" + def get_current_auto_os_patch_state(self): """ Gets the current auto OS update patch state on the machine """ - # NOTE: Implementation pending - pass + self.composite_logger.log("Fetching the current automatic OS patch state on the machine...") + + current_auto_os_patch_state_for_yum_cron = self.__get_current_auto_os_patch_state_for_yum_cron() + current_auto_os_patch_state_for_dnf_automatic = self.__get_current_auto_os_patch_state_for_dnf_automatic() + current_auto_os_patch_state_for_packagekit = self.__get_current_auto_os_patch_state_for_packagekit() + + self.composite_logger.log("OS patch state per auto OS update service: [yum-cron={0}] [dnf-automatic={1}] [packagekit={2}]" + .format(str(current_auto_os_patch_state_for_yum_cron), str(current_auto_os_patch_state_for_dnf_automatic), str(current_auto_os_patch_state_for_packagekit))) + + if current_auto_os_patch_state_for_yum_cron == Constants.AutomaticOSPatchStates.ENABLED \ + or current_auto_os_patch_state_for_dnf_automatic == Constants.AutomaticOSPatchStates.ENABLED \ + or current_auto_os_patch_state_for_packagekit == Constants.AutomaticOSPatchStates.ENABLED: + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.ENABLED + elif current_auto_os_patch_state_for_yum_cron == Constants.AutomaticOSPatchStates.DISABLED \ + and current_auto_os_patch_state_for_dnf_automatic == Constants.AutomaticOSPatchStates.DISABLED \ + and current_auto_os_patch_state_for_packagekit == Constants.AutomaticOSPatchStates.DISABLED: + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.DISABLED + else: + current_auto_os_patch_state = Constants.AutomaticOSPatchStates.UNKNOWN - def disable_auto_os_update(self): - """ Disables auto OS updates on the machine only if they are enabled and logs the default settings the machine comes with """ - # NOTE: Implementation pending - pass + self.composite_logger.log_debug("Overall Auto OS Patch State based on all auto OS update service states [OverallAutoOSPatchState={0}]".format(str(current_auto_os_patch_state))) + return current_auto_os_patch_state - def is_image_default_patch_configuration_backup_valid(self, image_default_patch_configuration_backup): - return True + def __get_current_auto_os_patch_state_for_yum_cron(self): + """ Gets current auto OS update patch state for yum-cron """ + self.composite_logger.log_debug("Fetching current automatic OS patch state in yum-cron service. This includes checks on whether the service is installed, current auto patch enable state and whether it is set to enable on reboot") + self.__init_auto_update_for_yum_cron() + is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value = self.__get_current_auto_os_updates_setting_on_machine() + + apply_updates = self.__get_extension_standard_value_for_apply_updates(apply_updates_value) + + if apply_updates == self.apply_updates_enabled or enable_on_reboot_value: + return Constants.AutomaticOSPatchStates.ENABLED + # OS patch state is considered to be disabled: a) if it was successfully disabled or b) if the service is not installed + elif not is_service_installed or (apply_updates == self.apply_updates_disabled and not enable_on_reboot_value): + return Constants.AutomaticOSPatchStates.DISABLED + else: + return Constants.AutomaticOSPatchStates.UNKNOWN + + def __get_current_auto_os_patch_state_for_dnf_automatic(self): + """ Gets current auto OS update patch state for dnf-automatic """ + self.composite_logger.log_debug("Fetching current automatic OS patch state in dnf-automatic service. This includes checks on whether the service is installed, current auto patch enable state and whether it is set to enable on reboot") + self.__init_auto_update_for_dnf_automatic() + is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value = self.__get_current_auto_os_updates_setting_on_machine() + + apply_updates = self.__get_extension_standard_value_for_apply_updates(apply_updates_value) + + if apply_updates == self.apply_updates_enabled or enable_on_reboot_value: + return Constants.AutomaticOSPatchStates.ENABLED + # OS patch state is considered to be disabled: a) if it was successfully disabled or b) if the service is not installed + elif not is_service_installed or (apply_updates == self.apply_updates_disabled and not enable_on_reboot_value): + return Constants.AutomaticOSPatchStates.DISABLED + else: + return Constants.AutomaticOSPatchStates.UNKNOWN + + def __get_current_auto_os_patch_state_for_packagekit(self): + """ Gets current auto OS update patch state for packagekit """ + self.composite_logger.log_debug("Fetching current automatic OS patch state in packagekit service. This includes checks on whether the service is installed, current auto patch enable state and whether it is set to enable on reboot") + self.__init_auto_update_for_packagekit() + is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value = self.__get_current_auto_os_updates_setting_on_machine() + + apply_updates = self.__get_extension_standard_value_for_apply_updates(apply_updates_value) + + if apply_updates == self.apply_updates_enabled or enable_on_reboot_value: + return Constants.AutomaticOSPatchStates.ENABLED + # OS patch state is considered to be disabled: a) if it was successfully disabled or b) if the service is not installed + elif not is_service_installed or (apply_updates == self.apply_updates_disabled and not enable_on_reboot_value): + return Constants.AutomaticOSPatchStates.DISABLED + else: + return Constants.AutomaticOSPatchStates.UNKNOWN + + def __get_extension_standard_value_for_apply_updates(self, apply_updates_value): + if apply_updates_value.lower() == 'yes' or apply_updates_value.lower() == 'true': + return self.apply_updates_enabled + elif apply_updates_value.lower() == 'no' or apply_updates_value.lower() == 'false': + return self.apply_updates_disabled + else: + return self.apply_updates_unknown + + def __init_auto_update_for_yum_cron(self): + """ Initializes all generic auto OS update variables with the config values for yum cron service """ + self.os_patch_configuration_settings_file_path = self.yum_cron_configuration_settings_file_path + self.download_updates_identifier_text = self.yum_cron_download_updates_identifier_text + self.apply_updates_identifier_text = self.yum_cron_apply_updates_identifier_text + self.enable_on_reboot_identifier_text = self.yum_cron_enable_on_reboot_identifier_text + self.installation_state_identifier_text = self.yum_cron_installation_state_identifier_text + self.auto_update_config_pattern_match_text = self.yum_cron_config_pattern_match_text + self.enable_on_reboot_check_cmd = self.yum_cron_enable_on_reboot_check_cmd + self.install_check_cmd = self.yum_cron_install_check_cmd + self.current_auto_os_update_service = Constants.YumAutoOSUpdateServices.YUM_CRON + + def __init_auto_update_for_dnf_automatic(self): + """ Initializes all generic auto OS update variables with the config values for dnf automatic service """ + self.os_patch_configuration_settings_file_path = self.dnf_automatic_configuration_file_path + self.download_updates_identifier_text = self.dnf_automatic_download_updates_identifier_text + self.apply_updates_identifier_text = self.dnf_automatic_apply_updates_identifier_text + self.enable_on_reboot_identifier_text = self.dnf_automatic_enable_on_reboot_identifier_text + self.installation_state_identifier_text = self.dnf_automatic_installation_state_identifier_text + self.auto_update_config_pattern_match_text = self.dnf_automatic_config_pattern_match_text + self.enable_on_reboot_check_cmd = self.dnf_automatic_enable_on_reboot_check_cmd + self.install_check_cmd = self.dnf_automatic_install_check_cmd + self.current_auto_os_update_service = Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC + + def __init_auto_update_for_packagekit(self): + """ Initializes all generic auto OS update variables with the config values for packagekit service """ + self.os_patch_configuration_settings_file_path = self.packagekit_configuration_file_path + self.download_updates_identifier_text = self.packagekit_download_updates_identifier_text + self.apply_updates_identifier_text = self.packagekit_apply_updates_identifier_text + self.enable_on_reboot_identifier_text = self.packagekit_enable_on_reboot_identifier_text + self.installation_state_identifier_text = self.packagekit_installation_state_identifier_text + self.auto_update_config_pattern_match_text = self.packagekit_config_pattern_match_text + self.enable_on_reboot_check_cmd = self.packagekit_enable_on_reboot_check_cmd + self.install_check_cmd = self.packagekit_install_check_cmd + self.current_auto_os_update_service = Constants.YumAutoOSUpdateServices.PACKAGEKIT + + def disable_auto_os_update(self): + """ Disables auto OS updates on the machine only if they are enable_on_reboot and logs the default settings the machine comes with """ + try: + self.composite_logger.log("Disabling auto OS updates in all identified services...") + self.disable_auto_os_update_for_yum_cron() + self.disable_auto_os_update_for_dnf_automatic() + self.disable_auto_os_update_for_packagekit() + self.composite_logger.log_debug("Successfully disabled auto OS updates") + + except Exception as error: + self.composite_logger.log_error("Could not disable auto OS updates. [Error={0}]".format(repr(error))) + raise + + def disable_auto_os_update_for_yum_cron(self): + """ Disables auto OS updates, using yum cron service, and logs the default settings the machine comes with """ + self.composite_logger.log("Disabling auto OS updates using yum-cron") + self.__init_auto_update_for_yum_cron() + + self.backup_image_default_patch_configuration_if_not_exists() + if not self.is_auto_update_service_installed(self.yum_cron_install_check_cmd): + self.composite_logger.log_debug("Cannot disable as yum-cron is not installed on the machine") + return + + self.composite_logger.log_debug("Preemptively disabling auto OS updates using yum-cron") + self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, "no", self.yum_cron_config_pattern_match_text) + self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, "no", self.yum_cron_config_pattern_match_text) + self.disable_auto_update_on_reboot(self.yum_cron_disable_on_reboot_cmd) + + self.composite_logger.log("Successfully disabled auto OS updates using yum-cron") + + def disable_auto_os_update_for_dnf_automatic(self): + """ Disables auto OS updates, using dnf-automatic service, and logs the default settings the machine comes with """ + self.composite_logger.log("Disabling auto OS updates using dnf-automatic") + self.__init_auto_update_for_dnf_automatic() + + self.backup_image_default_patch_configuration_if_not_exists() + + if not self.is_auto_update_service_installed(self.dnf_automatic_install_check_cmd): + self.composite_logger.log_debug("Cannot disable as dnf-automatic is not installed on the machine") + return + + self.composite_logger.log_debug("Preemptively disabling auto OS updates using dnf-automatic") + self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, "no", self.dnf_automatic_config_pattern_match_text) + self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, "no", self.dnf_automatic_config_pattern_match_text) + self.disable_auto_update_on_reboot(self.dnf_automatic_disable_on_reboot_cmd) + + self.composite_logger.log("Successfully disabled auto OS updates using dnf-automatic") + + def disable_auto_os_update_for_packagekit(self): + """ Disables auto OS updates, using packagekit service, and logs the default settings the machine comes with """ + self.composite_logger.log("Disabling auto OS updates using packagekit") + self.__init_auto_update_for_packagekit() + + self.backup_image_default_patch_configuration_if_not_exists() + + if not self.is_auto_update_service_installed(self.packagekit_install_check_cmd): + self.composite_logger.log_debug("Cannot disable as packagekit is not installed on the machine") + return + + self.composite_logger.log_debug("Preemptively disabling auto OS updates using packagekit") + #todo: uncomment after finding the correct value + # self.update_os_patch_configuration_sub_setting(self.download_updates_identifier_text, "false", self.packagekit_config_pattern_match_text) + self.update_os_patch_configuration_sub_setting(self.apply_updates_identifier_text, "false", self.packagekit_config_pattern_match_text) + self.disable_auto_update_on_reboot(self.dnf_automatic_disable_on_reboot_cmd) + + self.composite_logger.log("Successfully disabled auto OS updates using dnf-automatic") + + def is_service_set_to_enable_on_reboot(self, command): + """ Checking if auto update is enable_on_reboot on the machine. An enable_on_reboot service will be activated (if currently inactive) on machine reboot """ + self.composite_logger.log_debug("Checking if auto update service is set to enable on reboot...") + code, out = self.env_layer.run_command_output(command, False, False) + self.composite_logger.log_debug(" - Code: " + str(code) + ", Output: \n|\t" + "\n|\t".join(out.splitlines())) + if len(out.strip()) > 0 and code == 0 and 'enabled' in out: + self.composite_logger.log_debug("Auto OS update will enable on reboot") + return True + self.composite_logger.log_debug("Auto OS update will NOT enable on reboot") + return False def backup_image_default_patch_configuration_if_not_exists(self): """ Records the default system settings for auto OS updates within patch extension artifacts for future reference. We only log the default system settings a VM comes with, any subsequent updates will not be recorded""" - # NOTE: Implementation pending - pass + """ JSON format for backup file: + { + "yum-cron": { + "apply_updates": "yes/no/empty string", + "download_updates": "yes/no/empty string", + "enable_on_reboot": true/false, + "install_state": true/false + }, + "dnf-automatic": { + "apply_updates": "yes/no/empty string", + "download_updates": "yes/no/empty string", + "enable_on_reboot": true/false, + "install_state": true/false + }, + "packagekit": { + "WritePreparedUpdates": "true/false/empty string", + "GetPreparedUpdates": "true/false/empty string", //NOTE: This property name is pending validation as noted in another comment where the name is initialized + "enable_on_reboot": true/false, + "install_state": true/false + } + } """ + try: + self.composite_logger.log_debug("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 = {} + + # read existing backup since it also contains backup from other update services. We need to preserve any existing data within the backup file + if self.image_default_patch_configuration_backup_exists(): + try: + image_default_patch_configuration_backup = json.loads(self.env_layer.file_system.read_with_retry(self.image_default_patch_configuration_backup_path)) + except Exception as error: + self.composite_logger.log_error("Unable to read backup for default patch state. Will attempt to re-write. [Exception={0}]".format(repr(error))) + + # 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("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("Since the backup is invalid, will add a new backup with the current auto OS update settings") + self.composite_logger.log_debug("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() + + 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.enable_on_reboot_identifier_text: enable_on_reboot_value, + self.installation_state_identifier_text: is_service_installed + } + } + + image_default_patch_configuration_backup.update(backup_image_default_patch_configuration_json_to_add) + + self.composite_logger.log_debug("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+') + except Exception as error: + error_message = "Exception during fetching and logging default auto update settings on the machine. [Exception={0}]".format(repr(error)) + self.composite_logger.log_error(error_message) + self.status_handler.add_error_to_status(error_message, Constants.PatchOperationErrorCodes.DEFAULT_ERROR) + raise + + 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 """ + switcher = { + Constants.YumAutoOSUpdateServices.YUM_CRON: self.is_backup_valid_for_yum_cron, + Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC: self.is_backup_valid_for_dnf_automatic, + Constants.YumAutoOSUpdateServices.PACKAGEKIT: self.is_backup_valid_for_packagekit + } + try: + return switcher[self.current_auto_os_update_service](image_default_patch_configuration_backup) + except KeyError as e: + raise e + + def is_backup_valid_for_yum_cron(self, image_default_patch_configuration_backup): + if Constants.YumAutoOSUpdateServices.YUM_CRON in image_default_patch_configuration_backup \ + and self.yum_cron_download_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON] \ + and self.yum_cron_apply_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON] \ + and self.yum_cron_enable_on_reboot_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON] \ + and self.yum_cron_installation_state_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON]: + self.composite_logger.log_debug("Extension has a valid backup for default yum-cron configuration settings") + return True + else: + self.composite_logger.log_debug("Extension does not have a valid backup for default yum-cron configuration settings") + return False + + def is_backup_valid_for_dnf_automatic(self, image_default_patch_configuration_backup): + if Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC in image_default_patch_configuration_backup \ + and self.dnf_automatic_download_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC] \ + and self.dnf_automatic_apply_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC] \ + and self.dnf_automatic_enable_on_reboot_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC] \ + and self.dnf_automatic_installation_state_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC]: + self.composite_logger.log_debug("Extension has a valid backup for default dnf-automatic configuration settings") + return True + else: + self.composite_logger.log_debug("Extension does not have a valid backup for default dnf-automatic configuration settings") + return False + + def is_backup_valid_for_packagekit(self, image_default_patch_configuration_backup): + if Constants.YumAutoOSUpdateServices.PACKAGEKIT in image_default_patch_configuration_backup \ + and self.packagekit_download_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT] \ + and self.packagekit_apply_updates_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT] \ + and self.packagekit_enable_on_reboot_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT] \ + and self.packagekit_installation_state_identifier_text in image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT]: + self.composite_logger.log_debug("Extension has a valid backup for default packagekit configuration settings") + return True + else: + self.composite_logger.log_debug("Extension does not have a valid backup for default packagekit configuration settings") + return False + + def __get_current_auto_os_updates_setting_on_machine(self): + """ Gets all the update settings related to auto OS updates currently set on the machine """ + try: + download_updates_value = "" + apply_updates_value = "" + is_service_installed = False + enable_on_reboot_value = False + + # get install state + if not self.is_auto_update_service_installed(self.install_check_cmd): + self.composite_logger.log_debug("Auto OS service is not installed on the machine") + return is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value + + is_service_installed = True + enable_on_reboot_value = self.is_service_set_to_enable_on_reboot(self.enable_on_reboot_check_cmd) + + self.composite_logger.log_debug("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) + + if download_updates_value == "": + self.composite_logger.log_debug("Machine did not have any value set for [Setting={0}]".format(str(self.download_updates_identifier_text))) + else: + self.composite_logger.log_verbose("Current value set for [{0}={1}]".format(str(self.download_updates_identifier_text), str(download_updates_value))) + + if apply_updates_value == "": + self.composite_logger.log_debug("Machine did not have any value set for [Setting={0}]".format(str(self.apply_updates_identifier_text))) + else: + self.composite_logger.log_verbose("Current value set for [{0}={1}]".format(str(self.apply_updates_identifier_text), str(apply_updates_value))) + + return is_service_installed, enable_on_reboot_value, download_updates_value, apply_updates_value + + except Exception as error: + raise Exception("Error occurred in fetching current auto OS update settings from the machine. [Exception={0}]".format(repr(error))) + + def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value="no", config_pattern_match_text=""): + """ Updates (or adds if it doesn't exist) the given patch_configuration_sub_setting with the given value in os_patch_configuration_settings_file """ + 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 + self.composite_logger.log_debug("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) + patch_configuration_sub_setting_to_update = patch_configuration_sub_setting + ' = ' + value + patch_configuration_sub_setting_found_in_file = False + updated_patch_configuration_sub_setting = "" + settings = os_patch_configuration_settings.strip().split('\n') + + # update value of existing setting + for i in range(len(settings)): + match = re.search(patch_configuration_sub_setting + config_pattern_match_text, settings[i]) + if match is not None: + settings[i] = patch_configuration_sub_setting_to_update + patch_configuration_sub_setting_found_in_file = True + updated_patch_configuration_sub_setting += settings[i] + "\n" + + # add setting to configuration file, since it doesn't exist + if not patch_configuration_sub_setting_found_in_file: + updated_patch_configuration_sub_setting += patch_configuration_sub_setting_to_update + "\n" + + # ToDo: This adds some whitespace at the beginning of the first line in the settings file which is auto adjusted in the file later, so shouldn't have any issues right now. strip()/lstrip() on the string, does not work, will have to test accross versions and identify the impact + 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+') + except Exception as error: + error_msg = "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 + + def disable_auto_update_on_reboot(self, command): + self.composite_logger.log_debug("Disabling auto update on reboot using command: " + str(command)) + code, out = self.env_layer.run_command_output(command, False, False) + self.composite_logger.log_debug(" - Code: " + str(code) + ", Output: \n|\t" + "\n|\t".join(out.splitlines())) + + if code != 0: + self.composite_logger.log('[ERROR] Command invoked: ' + command) + self.telemetry_writer.write_execution_error(command, code, out) + error_msg = 'Unexpected return code (' + str(code) + ') on command: ' + command + 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("Auto update on reboot disabled") + + def is_auto_update_service_installed(self, install_check_cmd): + """ Checks if the auto update service is enable_on_reboot on the VM """ + self.composite_logger.log_debug("Checking if auto update service is installed...") + code, out = self.env_layer.run_command_output(install_check_cmd, False, False) + self.composite_logger.log_debug(" - Code: " + str(code) + ", Output: \n|\t" + "\n|\t".join(out.splitlines())) + if len(out.strip()) > 0 and code == 0: + self.composite_logger.log_debug("Auto OS update is installed on the machine") + return True + else: + self.composite_logger.log_debug("Auto OS update is NOT installed on the machine") + return False - def update_os_patch_configuration_sub_setting(self, patch_configuration_sub_setting, value): - # NOTE: Implementation pending - pass # endregion # region Handling known errors diff --git a/src/core/tests/Test_AptitudePackageManager.py b/src/core/tests/Test_AptitudePackageManager.py index 3f108510..81ee0a16 100644 --- a/src/core/tests/Test_AptitudePackageManager.py +++ b/src/core/tests/Test_AptitudePackageManager.py @@ -32,7 +32,7 @@ def tearDown(self): def mock_read_with_retry_raise_exception(self): raise Exception - def mock_write_with_retry_raise_exception(self): + def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode='a+'): raise Exception def test_package_manager_no_updates(self): @@ -179,24 +179,19 @@ def test_image_default_patch_mode_backup_exists(self): 'APT::Periodic::Unattended-Upgrade': "1" } self.runtime.env_layer.file_system.write_with_retry(package_manager.image_default_patch_configuration_backup_path, '{0}'.format(json.dumps(image_default_patch_configuration_backup)), mode='w+') + 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.assertTrue(package_manager.image_default_patch_configuration_backup_exists()) + self.assertTrue(package_manager.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup)) - # invalid or no patch mode backup + # invalid mode backup image_default_patch_configuration_backup = '[]' self.runtime.env_layer.file_system.write_with_retry(package_manager.image_default_patch_configuration_backup_path, '{0}'.format(json.dumps(image_default_patch_configuration_backup)), mode='w+') - self.assertFalse(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.assertTrue(package_manager.image_default_patch_configuration_backup_exists()) + self.assertFalse(package_manager.is_image_default_patch_configuration_backup_valid(image_default_patch_configuration_backup)) def test_image_default_patch_mode_backup_does_not_exist(self): package_manager = self.container.get('package_manager') - read_with_retry_backup = self.runtime.env_layer.file_system.read_with_retry - self.runtime.env_layer.file_system.read_with_retry = self.mock_read_with_retry_raise_exception - - # ensure valid log file exists - os_patch_mode_settings = 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' - package_manager.os_patch_mode_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "20auto-upgrades") - self.runtime.write_to_file(package_manager.os_patch_mode_settings_file_path, os_patch_mode_settings) - self.assertFalse(package_manager.image_default_patch_configuration_backup_exists()) - self.runtime.env_layer.file_system.read_with_retry = read_with_retry_backup # file does not exist package_manager.image_default_patch_mode_backup_path = "tests" @@ -252,6 +247,25 @@ def test_backup_image_default_patch_mode_with_default_patch_mode_set(self): self.assertTrue(image_default_patch_configuration_backup['APT::Periodic::Update-Package-Lists'] == "1") self.assertTrue(image_default_patch_configuration_backup['APT::Periodic::Unattended-Upgrade'] == "1") + def test_backup_image_default_patch_mode_overwrite_backup_if_original_backup_was_invalid(self): + package_manager = self.container.get('package_manager') + package_manager.os_patch_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "20auto-upgrades") + + # backup file exists but the content is invalid, function should overwrite the file with valid content + os_patch_configuration_settings = 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' + self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, os_patch_configuration_settings) + + existing_image_default_backup_configuration = '[]' + self.runtime.write_to_file(package_manager.image_default_patch_configuration_backup_path, existing_image_default_backup_configuration) + self.assertTrue(package_manager.image_default_patch_configuration_backup_exists()) + self.assertFalse(package_manager.is_image_default_patch_configuration_backup_valid(existing_image_default_backup_configuration)) + + package_manager.backup_image_default_patch_configuration_if_not_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.assertTrue(image_default_patch_configuration_backup is not None) + self.assertTrue(image_default_patch_configuration_backup['APT::Periodic::Update-Package-Lists'] == "1") + self.assertTrue(image_default_patch_configuration_backup['APT::Periodic::Unattended-Upgrade'] == "1") + def test_backup_image_default_patch_mode_with_default_patch_mode_not_set(self): package_manager = self.container.get('package_manager') package_manager.os_patch_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "20auto-upgrades") @@ -266,10 +280,10 @@ def test_backup_image_default_patch_mode_with_default_patch_mode_not_set(self): def test_backup_image_default_patch_mode_raises_exception(self): package_manager = self.container.get('package_manager') - package_manager.os_patch_mode_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "20auto-upgrades") + package_manager.os_patch_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "20auto-upgrades") # default system patch mode is set, write to log os_patch_mode_settings = 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' - self.runtime.write_to_file(package_manager.os_patch_mode_settings_file_path, os_patch_mode_settings) + self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, os_patch_mode_settings) 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) diff --git a/src/core/tests/Test_YumPackageManager.py b/src/core/tests/Test_YumPackageManager.py index df82795f..657c4894 100644 --- a/src/core/tests/Test_YumPackageManager.py +++ b/src/core/tests/Test_YumPackageManager.py @@ -13,7 +13,8 @@ # limitations under the License. # # Requires Python 2.7+ - +import json +import os import unittest from core.src.bootstrap.Constants import Constants from core.tests.library.ArgumentComposer import ArgumentComposer @@ -28,6 +29,9 @@ def setUp(self): def tearDown(self): self.runtime.stop() + def mock_write_with_retry_raise_exception(self, file_path_or_handle, data, mode='a+'): + raise Exception + def test_package_manager_no_updates(self): """Unit test for yum package manager with no updates""" # Path change @@ -459,6 +463,126 @@ def test_ssl_certificate_issue_type3_fix_fail(self): self.assertRaises(Exception, package_manager.invoke_package_manager, package_manager.yum_check) + def test_disable_auto_os_updates_with_uninstalled_services(self): + # no services are installed on the machine. expected o/p: function will complete successfully. Backup file will be created with default values, no auto OS update configuration settings will be updated as there are none + self.runtime.set_legacy_test_type('SadPath') + package_manager = self.container.get('package_manager') + 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.assertTrue(image_default_patch_configuration_backup is not None) + + # validating backup for yum-cron + self.assertTrue(Constants.YumAutoOSUpdateServices.YUM_CRON in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_download_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_apply_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_enable_on_reboot_identifier_text], False) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_installation_state_identifier_text], False) + + # validating backup for dnf-automatic + self.assertTrue(Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_download_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_apply_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_enable_on_reboot_identifier_text], False) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_installation_state_identifier_text], False) + + # validating backup for packagekit + self.assertTrue(Constants.YumAutoOSUpdateServices.PACKAGEKIT in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_download_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_apply_updates_identifier_text], "") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_enable_on_reboot_identifier_text], False) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_installation_state_identifier_text], False) + + def test_disable_auto_os_updates_with_installed_services(self): + # all services are installed and contain valid configurations. expected o/p All services will be disabled and backup file should reflect default settings for all + self.runtime.set_legacy_test_type('HappyPath') + package_manager = self.container.get('package_manager') + + package_manager.yum_cron_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "yum-cron.conf") + yum_cron_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' + self.runtime.write_to_file(package_manager.yum_cron_configuration_settings_file_path, yum_cron_os_patch_configuration_settings) + + package_manager.dnf_automatic_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, "automatic.conf") + dnf_automatic_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' + self.runtime.write_to_file(package_manager.dnf_automatic_configuration_file_path, dnf_automatic_os_patch_configuration_settings) + + package_manager.packagekit_configuration_file_path = os.path.join(self.runtime.execution_config.config_folder, "PackageKit.conf") + packagekit_os_patch_configuration_settings = 'WritePreparedUpdates = true\nGetPreparedUpdates = true\n' + self.runtime.write_to_file(package_manager.packagekit_configuration_file_path, packagekit_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.assertTrue(image_default_patch_configuration_backup is not None) + + # validating backup for yum-cron + self.assertTrue(Constants.YumAutoOSUpdateServices.YUM_CRON in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_download_updates_identifier_text], "yes") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_apply_updates_identifier_text], "yes") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_enable_on_reboot_identifier_text], True) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.YUM_CRON][package_manager.yum_cron_installation_state_identifier_text], True) + + # validating backup for dnf-automatic + self.assertTrue(Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_download_updates_identifier_text], "yes") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_apply_updates_identifier_text], "yes") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_enable_on_reboot_identifier_text], True) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.DNF_AUTOMATIC][package_manager.dnf_automatic_installation_state_identifier_text], True) + + # validating backup for packagekit + self.assertTrue(Constants.YumAutoOSUpdateServices.PACKAGEKIT in image_default_patch_configuration_backup) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_download_updates_identifier_text], "true") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_apply_updates_identifier_text], "true") + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_enable_on_reboot_identifier_text], True) + self.assertEquals(image_default_patch_configuration_backup[Constants.YumAutoOSUpdateServices.PACKAGEKIT][package_manager.packagekit_installation_state_identifier_text], True) + + def test_disable_auto_os_update_failure(self): + # disable with non existing log file + package_manager = self.container.get('package_manager') + + self.assertRaises(Exception, package_manager.disable_auto_os_update) + self.assertTrue(package_manager.image_default_patch_configuration_backup_exists()) + + 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.yum_cron_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "yum-cron.conf") + + # disable apply_udpates when enabled by default + yum_cron_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' + self.runtime.write_to_file(package_manager.yum_cron_configuration_settings_file_path, yum_cron_os_patch_configuration_settings) + + package_manager.update_os_patch_configuration_sub_setting(package_manager.yum_cron_apply_updates_identifier_text, "no", package_manager.yum_cron_config_pattern_match_text) + yum_cron_os_patch_configuration_settings_file_path_read = self.runtime.env_layer.file_system.read_with_retry(package_manager.yum_cron_configuration_settings_file_path) + self.assertTrue(yum_cron_os_patch_configuration_settings_file_path_read is not None) + self.assertTrue('apply_updates = no' in yum_cron_os_patch_configuration_settings_file_path_read) + self.assertTrue('download_updates = yes' in yum_cron_os_patch_configuration_settings_file_path_read) + + # disable download_updates when enabled by default + yum_cron_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' + self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, yum_cron_os_patch_configuration_settings) + package_manager.update_os_patch_configuration_sub_setting(package_manager.yum_cron_download_updates_identifier_text, "no", package_manager.yum_cron_config_pattern_match_text) + yum_cron_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.assertTrue(yum_cron_os_patch_configuration_settings_file_path_read is not None) + self.assertTrue('apply_updates = yes' in yum_cron_os_patch_configuration_settings_file_path_read) + self.assertTrue('download_updates = no' in yum_cron_os_patch_configuration_settings_file_path_read) + + # disable apply_updates when default patch mode settings file is empty + yum_cron_os_patch_configuration_settings = '' + self.runtime.write_to_file(package_manager.os_patch_configuration_settings_file_path, yum_cron_os_patch_configuration_settings) + package_manager.update_os_patch_configuration_sub_setting(package_manager.yum_cron_apply_updates_identifier_text, "no", package_manager.yum_cron_config_pattern_match_text) + yum_cron_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.assertTrue(yum_cron_os_patch_configuration_settings_file_path_read is not None) + self.assertTrue('download_updates' not in yum_cron_os_patch_configuration_settings_file_path_read) + self.assertTrue('apply_updates = no' in yum_cron_os_patch_configuration_settings_file_path_read) + + def test_update_image_default_patch_mode_raises_exception(self): + package_manager = self.container.get('package_manager') + package_manager.yum_cron_configuration_settings_file_path = os.path.join(self.runtime.execution_config.config_folder, "yum-cron.conf") + yum_cron_os_patch_configuration_settings = 'apply_updates = yes\ndownload_updates = yes\n' + self.runtime.write_to_file(package_manager.yum_cron_configuration_settings_file_path, yum_cron_os_patch_configuration_settings) + 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) + if __name__ == '__main__': unittest.main() diff --git a/src/core/tests/library/LegacyEnvLayerExtensions.py b/src/core/tests/library/LegacyEnvLayerExtensions.py index 66d63509..f148baa4 100644 --- a/src/core/tests/library/LegacyEnvLayerExtensions.py +++ b/src/core/tests/library/LegacyEnvLayerExtensions.py @@ -401,6 +401,15 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): "util-linux-2.23.2-52.el7.x86_64\n" + \ "803 agetty 0:00 848 kB Sleeping: *17:00\n" + \ " 804 agetty 0:00 868 kB Sleeping: *17:00\n" + elif cmd.find("systemctl list-unit-files --type=service") > -1: + code = 0 + output = 'Auto update service installed' + elif cmd.find("systemctl is-enabled ") > -1: + code = 0 + output = 'enabled' + elif cmd.find("systemctl disable ") > -1: + code = 0 + output = 'Auto update service disabled' elif self.legacy_package_manager_name is Constants.APT: if cmd.find("dist-upgrade") > -1: code = 0 @@ -503,6 +512,9 @@ def run_command_output(self, cmd, no_output=False, chk_err=True): if cmd.find('ps --forest -o pid,cmd -g $(ps -o sid= -p') > -1: output = 'test' code = 1 + elif cmd.find("systemctl") > -1: + code = 1 + output = '' elif self.legacy_test_type == 'UnalignedPath': if cmd.find("cat /proc/cpuinfo | grep name") > -1: code = 0 diff --git a/src/extension/src/Constants.py b/src/extension/src/Constants.py index 4a3f6fb8..6163bd5a 100644 --- a/src/extension/src/Constants.py +++ b/src/extension/src/Constants.py @@ -28,7 +28,7 @@ def __iter__(self): yield item # Extension version (todo: move to a different file) - EXT_VERSION = "1.6.26" + EXT_VERSION = "1.6.27" # Runtime environments TEST = 'Test' diff --git a/src/extension/src/file_handlers/ExtOutputStatusHandler.py b/src/extension/src/file_handlers/ExtOutputStatusHandler.py index a44b3855..d79ec413 100644 --- a/src/extension/src/file_handlers/ExtOutputStatusHandler.py +++ b/src/extension/src/file_handlers/ExtOutputStatusHandler.py @@ -27,7 +27,7 @@ "timestampUTC": "2019-07-20T12:12:14Z", "status": { "name": "Azure Patch Management", - "operation": "Assessment / Deployment / NoOperation", + "operation": "Assessment / Installation / NoOperation / ConfigurePatching", "status": "transitioning / error / success / warning", "code": 0, "formattedMessage": { diff --git a/src/extension/src/manifest.xml b/src/extension/src/manifest.xml index b7dd95e4..8a54e460 100644 --- a/src/extension/src/manifest.xml +++ b/src/extension/src/manifest.xml @@ -2,7 +2,7 @@ Microsoft.CPlat.Core LinuxPatchExtension - 1.6.26 + 1.6.27 VmRole