diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index a59fb58d..39bb6bf5 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.47" + EXT_VERSION = "1.6.48" # Runtime environments TEST = 'Test' @@ -51,9 +51,12 @@ def __iter__(self): MAX_AUTO_ASSESSMENT_LOGFILE_SIZE_IN_BYTES = 5*1024*1024 MAX_AUTO_ASSESSMENT_WAIT_FOR_MAIN_CORE_EXEC_IN_MINUTES = 3 * 60 - class Paths(EnumBackport): + class SystemPaths(EnumBackport): SYSTEMD_ROOT = "/etc/systemd/system/" + class AzGPSPaths(EnumBackport): + EULA_SETTINGS = "/var/lib/azure/linuxpatchextension/patch.eula.settings" + class EnvSettings(EnumBackport): LOG_FOLDER = "logFolder" CONFIG_FOLDER = "configFolder" @@ -77,6 +80,11 @@ class ConfigSettings(EnumBackport): ASSESSMENT_MODE = 'assessmentMode' MAXIMUM_ASSESSMENT_INTERVAL = 'maximumAssessmentInterval' + class EulaSettings(EnumBackport): + ACCEPT_EULA_FOR_ALL_PATCHES = 'AcceptEULAForAllPatches' + ACCEPTED_BY = 'AcceptedBy' + LAST_MODIFIED = 'LastModified' + TEMP_FOLDER_DIR_NAME = "tmp" TEMP_FOLDER_CLEANUP_ARTIFACT_LIST = ["*.list"] @@ -201,6 +209,7 @@ class AutoAssessmentStates(EnumBackport): MAX_IMDS_CONNECTION_RETRY_COUNT = 5 MAX_ZYPPER_REPO_REFRESH_RETRY_COUNT = 5 MAX_BATCH_SIZE_FOR_PACKAGES = 3 + MAX_COMPLETE_STATUS_FILES_TO_RETAIN = 10 class PackageClassification(EnumBackport): UNCLASSIFIED = 'Unclassified' diff --git a/src/core/src/core_logic/ExecutionConfig.py b/src/core/src/core_logic/ExecutionConfig.py index 9f4ef958..48044e67 100644 --- a/src/core/src/core_logic/ExecutionConfig.py +++ b/src/core/src/core_logic/ExecutionConfig.py @@ -86,6 +86,9 @@ def __init__(self, env_layer, composite_logger, execution_parameters): else: self.composite_logger.log_debug("Not executing in auto-assessment mode.") + # EULA config + self.accept_package_eula = self.__is_eula_accepted_for_all_patches() + def __transform_execution_config_for_auto_assessment(self): self.activity_id = str(uuid.uuid4()) self.included_classifications_list = self.included_package_name_mask_list = self.excluded_package_name_mask_list = [] @@ -179,3 +182,31 @@ def __check_and_create_temp_folder_if_not_exists(self): self.composite_logger.log_debug("Temp folder does not exist, creating one from extension core. [Path={0}]".format(str(self.temp_folder))) os.mkdir(self.temp_folder) + def __is_eula_accepted_for_all_patches(self): + """ Reads customer provided config on EULA acceptance from disk and returns a boolean. + NOTE: This is a temporary solution and will be deprecated soon """ + is_eula_accepted = False + try: + if os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS): + eula_settings = json.loads(self.env_layer.file_system.read_with_retry(Constants.AzGPSPaths.EULA_SETTINGS) or 'null') + accept_eula_for_all_patches = self.__fetch_specific_eula_setting(eula_settings, Constants.EulaSettings.ACCEPT_EULA_FOR_ALL_PATCHES) + accepted_by = self.__fetch_specific_eula_setting(eula_settings, Constants.EulaSettings.ACCEPTED_BY) + last_modified = self.__fetch_specific_eula_setting(eula_settings, Constants.EulaSettings.LAST_MODIFIED) + if accept_eula_for_all_patches is not None and accept_eula_for_all_patches in [True, 'True', 'true', '1', 1]: + is_eula_accepted = True + self.composite_logger.log_debug("EULA config values from disk: [AcceptEULAForAllPatches={0}] [AcceptedBy={1}] [LastModified={2}]. Computed value of [IsEULAAccepted={3}]" + .format(str(accept_eula_for_all_patches), str(accepted_by), str(last_modified), str(is_eula_accepted))) + else: + self.composite_logger.log_debug("No EULA Settings found on the VM. Computed value of [IsEULAAccepted={0}]".format(str(is_eula_accepted))) + except Exception as error: + self.composite_logger.log_debug("Error occurred while reading and parsing EULA settings. Not accepting EULA for any patch. Error=[{0}]".format(repr(error))) + + return is_eula_accepted + + @staticmethod + def __fetch_specific_eula_setting(settings_source, setting_to_fetch): + """ Returns the specific setting value from eula_settings_source or None if not found """ + if settings_source is not None and setting_to_fetch is not None and setting_to_fetch in settings_source: + return settings_source[setting_to_fetch] + return None + diff --git a/src/core/src/core_logic/SystemctlManager.py b/src/core/src/core_logic/SystemctlManager.py index 5d10d3eb..73e125d5 100644 --- a/src/core/src/core_logic/SystemctlManager.py +++ b/src/core/src/core_logic/SystemctlManager.py @@ -31,7 +31,7 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.service_desc = service_info.service_desc self.service_exec_path = service_info.service_exec_path - self.__systemd_path = Constants.Paths.SYSTEMD_ROOT + self.__systemd_path = Constants.SystemPaths.SYSTEMD_ROOT self.systemctl_daemon_reload_cmd = "sudo systemctl daemon-reload" self.systemctl_version = "systemctl --version" diff --git a/src/core/src/package_managers/AptitudePackageManager.py b/src/core/src/package_managers/AptitudePackageManager.py index df21aa53..737de12b 100644 --- a/src/core/src/package_managers/AptitudePackageManager.py +++ b/src/core/src/package_managers/AptitudePackageManager.py @@ -34,6 +34,10 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ super(AptitudePackageManager, self).__init__(env_layer, execution_config, composite_logger, telemetry_writer, status_handler) security_list_guid = str(uuid.uuid4()) + + # Accept EULA (End User License Agreement) as per the EULA settings set by user + optional_accept_eula_in_cmd = "ACCEPT_EULA=Y" if execution_config.accept_package_eula else "" + # Repo refresh self.repo_refresh = 'sudo apt-get -q update' @@ -44,12 +48,12 @@ def __init__(self, env_layer, execution_config, composite_logger, telemetry_writ self.single_package_check_versions = 'apt-cache madison ' self.single_package_find_installed_dpkg = 'sudo dpkg -s ' self.single_package_find_installed_apt = 'sudo apt list --installed ' - self.single_package_upgrade_simulation_cmd = '''DEBIAN_FRONTEND=noninteractive apt-get -y --only-upgrade true -s install ''' - self.single_package_dependency_resolution_template = 'DEBIAN_FRONTEND=noninteractive LANG=en_US.UTF8 apt-get -y --only-upgrade true -s install ' + self.single_package_upgrade_simulation_cmd = '''DEBIAN_FRONTEND=noninteractive ''' + optional_accept_eula_in_cmd + ''' apt-get -y --only-upgrade true -s install ''' + self.single_package_dependency_resolution_template = 'DEBIAN_FRONTEND=noninteractive ' + optional_accept_eula_in_cmd + ' LANG=en_US.UTF8 apt-get -y --only-upgrade true -s install ' # Install update # --only-upgrade: upgrade only single package (only if it is installed) - self.single_package_upgrade_cmd = '''sudo DEBIAN_FRONTEND=noninteractive apt-get -y --only-upgrade true install ''' + self.single_package_upgrade_cmd = '''sudo DEBIAN_FRONTEND=noninteractive ''' + optional_accept_eula_in_cmd + ''' apt-get -y --only-upgrade true install ''' # Package manager exit code(s) self.apt_exitcode_ok = 0 @@ -476,8 +480,9 @@ def get_package_size(self, output): def get_current_auto_os_patch_state(self): """ Gets the current auto OS update patch state on the machine """ 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: + if os.path.exists(self.os_patch_configuration_settings_file_path): + self.__get_current_auto_os_updates_setting_on_machine() + if not os.path.exists(self.os_patch_configuration_settings_file_path) or int(self.unattended_upgrade_value) == 0: current_auto_os_patch_state = Constants.AutomaticOSPatchStates.DISABLED elif int(self.unattended_upgrade_value) == 1: current_auto_os_patch_state = Constants.AutomaticOSPatchStates.ENABLED diff --git a/src/core/src/service_interfaces/StatusHandler.py b/src/core/src/service_interfaces/StatusHandler.py index 46fd4cdd..76c5c5ef 100644 --- a/src/core/src/service_interfaces/StatusHandler.py +++ b/src/core/src/service_interfaces/StatusHandler.py @@ -14,6 +14,7 @@ # # Requires Python 2.7+ import collections +import glob import json import os import re @@ -323,7 +324,7 @@ def set_assessment_substatus_json(self, status=Constants.STATUS_TRANSITIONING, c self.__assessment_substatus_json = self.__new_substatus_json_for_operation(Constants.PATCH_ASSESSMENT_SUMMARY, status, code, json.dumps(self.__assessment_summary_json)) # Update complete status on disk - self.__write_complete_status_file() + self.__write_status_file() def __new_assessment_summary_json(self, assessment_packages_json, status, code): """ Called by: set_assessment_substatus_json @@ -372,7 +373,7 @@ def set_installation_substatus_json(self, status=Constants.STATUS_TRANSITIONING, self.__installation_substatus_json = self.__new_substatus_json_for_operation(Constants.PATCH_INSTALLATION_SUMMARY, status, code, json.dumps(self.__installation_summary_json)) # Update complete status on disk - self.__write_complete_status_file() + self.__write_status_file() def __new_installation_summary_json(self, installation_packages_json): """ Called by: set_installation_substatus_json @@ -434,7 +435,7 @@ def set_patch_metadata_for_healthstore_substatus_json(self, status=Constants.STA self.__metadata_for_healthstore_substatus_json = self.__new_substatus_json_for_operation(Constants.PATCH_METADATA_FOR_HEALTHSTORE, status, code, json.dumps(self.__metadata_for_healthstore_summary_json)) # Update complete status on disk - self.__write_complete_status_file() + self.__write_status_file() # wait period required in cases where we need to ensure HealthStore reads the status from GA if wait_after_update: @@ -467,7 +468,7 @@ def set_configure_patching_substatus_json(self, status=Constants.STATUS_TRANSITI self.__configure_patching_substatus_json = self.__new_substatus_json_for_operation(Constants.CONFIGURE_PATCHING_SUMMARY, status, code, json.dumps(self.__configure_patching_summary_json)) # Update complete status on disk - self.__write_complete_status_file() + self.__write_status_file() def __new_configure_patching_summary_json(self, automatic_os_patch_state, auto_assessment_state, status, code): """ Called by: set_configure_patching_substatus_json @@ -569,6 +570,9 @@ def load_status_file_components(self, initial_load=False): self.composite_logger.log_debug("Loading status file components [InitialLoad={0}].".format(str(initial_load))) + # Remove older complete status files + self.__removed_older_complete_status_files(self.execution_config.status_folder) + # Verify the status file exists - if not, reset status file if not os.path.exists(self.complete_status_file_path) and initial_load: self.composite_logger.log_warning("Status file not found at initial load. Resetting status file to defaults.") @@ -591,8 +595,7 @@ def load_status_file_components(self, initial_load=False): if self.execution_config.exec_auto_assess_only: self.__installation_substatus_json = complete_status_file_data['status']['substatus'][i] else: - message = complete_status_file_data['status']['substatus'][i]['formattedMessage']['message'] - self.__installation_summary_json = json.loads(message) + self.__installation_summary_json = self.__get_substatus_message(complete_status_file_data, i) self.__installation_packages_map = collections.OrderedDict((package["patchId"], package) for package in self.__installation_summary_json['patches']) self.__installation_packages = list(self.__installation_packages_map.values()) self.__maintenance_window_exceeded = bool(self.__installation_summary_json['maintenanceWindowExceeded']) @@ -602,8 +605,7 @@ def load_status_file_components(self, initial_load=False): self.__installation_errors = errors['details'] self.__installation_total_error_count = self.__get_total_error_count_from_prev_status(errors['message']) if name == Constants.PATCH_ASSESSMENT_SUMMARY: # if it exists, it must be to spec, or an exception will get thrown - message = complete_status_file_data['status']['substatus'][i]['formattedMessage']['message'] - self.__assessment_summary_json = json.loads(message) + self.__assessment_summary_json = self.__get_substatus_message(complete_status_file_data, i) self.__assessment_packages_map = collections.OrderedDict((package["patchId"], package) for package in self.__assessment_summary_json['patches']) self.__assessment_packages = list(self.__assessment_packages_map.values()) errors = self.__assessment_summary_json['errors'] @@ -614,19 +616,20 @@ def load_status_file_components(self, initial_load=False): if self.execution_config.exec_auto_assess_only: self.__metadata_for_healthstore_substatus_json = complete_status_file_data['status']['substatus'][i] else: - message = complete_status_file_data['status']['substatus'][i]['formattedMessage']['message'] - self.__metadata_for_healthstore_summary_json = json.loads(message) + self.__metadata_for_healthstore_summary_json = self.__get_substatus_message(complete_status_file_data, i) if name == Constants.CONFIGURE_PATCHING_SUMMARY: # if it exists, it must be to spec, or an exception will get thrown if self.execution_config.exec_auto_assess_only: self.__configure_patching_substatus_json = complete_status_file_data['status']['substatus'][i] else: - message = complete_status_file_data['status']['substatus'][i]['formattedMessage']['message'] - self.__configure_patching_summary_json = json.loads(message) + self.__configure_patching_summary_json = self.__get_substatus_message(complete_status_file_data, i) errors = self.__configure_patching_summary_json['errors'] if errors is not None and errors['details'] is not None: self.__configure_patching_errors = errors['details'] self.__configure_patching_top_level_error_count = self.__get_total_error_count_from_prev_status(errors['message']) + def __get_substatus_message(self, status_file_data, index): + return json.loads(status_file_data['status']['substatus'][index]['formattedMessage']['message']) + def __load_complete_status_file_data(self, file_path): # Read the status file - raise exception on persistent failure for i in range(0, Constants.MAX_FILE_OPERATION_RETRY_COUNT): @@ -641,7 +644,7 @@ def __load_complete_status_file_data(self, file_path): raise return complete_status_file_data - def __write_complete_status_file(self): + def __write_status_file(self): """ Composes and writes the status file from **already up-to-date** in-memory data. This is usually the final call to compose and persist after an in-memory data update in a specialized method. @@ -806,3 +809,21 @@ def __set_errors_json(self, error_count_by_operation, errors_by_operation): } # endregion + def __removed_older_complete_status_files(self, status_folder): + """ Retain 10 latest status complete file and remove other .complete.status files """ + files_removed = [] + all_complete_status_files = glob.glob(os.path.join(status_folder, '*.complete.status')) # Glob return empty list if no file matched pattern + if len(all_complete_status_files) <= Constants.MAX_COMPLETE_STATUS_FILES_TO_RETAIN: + return + + all_complete_status_files.sort(key=os.path.getmtime, reverse=True) + for complete_status_file in all_complete_status_files[Constants.MAX_COMPLETE_STATUS_FILES_TO_RETAIN:]: + try: + if os.path.exists(complete_status_file): + os.remove(complete_status_file) + files_removed.append(complete_status_file) + except Exception as e: + self.composite_logger.log_debug("Error deleting complete status file. [File={0} [Exception={1}]]".format(repr(complete_status_file), repr(e))) + + self.composite_logger.log_debug("Cleaned up older complete status files: {0}".format(files_removed)) + diff --git a/src/core/tests/Test_AptitudePackageManager.py b/src/core/tests/Test_AptitudePackageManager.py index f15fb6dd..62992185 100644 --- a/src/core/tests/Test_AptitudePackageManager.py +++ b/src/core/tests/Test_AptitudePackageManager.py @@ -17,6 +17,7 @@ import os import unittest from core.src.bootstrap.Constants import Constants +from core.src.core_logic.ExecutionConfig import ExecutionConfig from core.tests.Test_UbuntuProClient import MockVersionResult, MockRebootRequiredResult, MockUpdatesResult from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions @@ -26,7 +27,8 @@ class TestAptitudePackageManager(unittest.TestCase): def setUp(self): - self.runtime = RuntimeCompositor(ArgumentComposer().get_composed_arguments(), True, Constants.APT) + self.argument_composer = ArgumentComposer().get_composed_arguments() + self.runtime = RuntimeCompositor(self.argument_composer, True, Constants.APT) self.container = self.runtime.container def tearDown(self): @@ -63,7 +65,7 @@ def mock_os_path_isfile_raise_exception(self, file): def mock_get_security_updates_return_empty_list(self): return [], [] - #endregion Mocks + # endregion Mocks def test_package_manager_no_updates(self): """Unit test for aptitude package manager with no updates""" @@ -214,6 +216,15 @@ def test_disable_auto_os_update_with_one_patch_mode_enabled_success(self): self.assertTrue('APT::Periodic::Update-Package-Lists "0"' in os_patch_configuration_settings) self.assertTrue('APT::Periodic::Unattended-Upgrade "0"' in os_patch_configuration_settings) + def test_get_current_auto_os_updates_with_no_os_patch_configuration_settings_file(self): + # os_patch_configuration_settings_file does not exist, hence current os patch state is marked as Disabled + package_manager = self.container.get('package_manager') + package_manager.get_current_auto_os_patch_state = self.runtime.backup_get_current_auto_os_patch_state + + self.assertTrue(package_manager.get_current_auto_os_patch_state() == Constants.AutomaticOSPatchStates.DISABLED) + + package_manager.get_current_auto_os_patch_state = self.runtime.get_current_auto_os_patch_state + def test_disable_auto_os_update_failure(self): # disable with non existing log file package_manager = self.container.get('package_manager') @@ -561,6 +572,238 @@ def test_check_pro_client_prerequisites_should_return_false(self): LegacyEnvLayerExtensions.LegacyPlatform.linux_distribution = backup_envlayer_platform_linux_distribution package_manager.ubuntu_pro_client.is_pro_working = backup_ubuntu_pro_client_is_pro_working + def test_eula_accepted_for_patches(self): + # EULA accepted in settings and commands updated accordingly + self.runtime.execution_config.accept_package_eula = True + package_manager_for_test = AptitudePackageManager.AptitudePackageManager(self.runtime.env_layer, self.runtime.execution_config, self.runtime.composite_logger, self.runtime.telemetry_writer, self.runtime.status_handler) + self.assertTrue("ACCEPT_EULA=Y" in package_manager_for_test.single_package_upgrade_simulation_cmd) + self.assertTrue("ACCEPT_EULA=Y" in package_manager_for_test.single_package_dependency_resolution_template) + self.assertTrue("ACCEPT_EULA=Y" in package_manager_for_test.single_package_upgrade_cmd) + + def test_eula_not_accepted_for_patches(self): + # EULA accepted in settings and commands updated accordingly + self.runtime.execution_config.accept_package_eula = False + package_manager_for_test = AptitudePackageManager.AptitudePackageManager(self.runtime.env_layer, self.runtime.execution_config, self.runtime.composite_logger, self.runtime.telemetry_writer, self.runtime.status_handler) + self.assertTrue("ACCEPT_EULA=Y" not in package_manager_for_test.single_package_upgrade_simulation_cmd) + self.assertTrue("ACCEPT_EULA=Y" not in package_manager_for_test.single_package_dependency_resolution_template) + self.assertTrue("ACCEPT_EULA=Y" not in package_manager_for_test.single_package_upgrade_cmd) + + def test_eula_acceptance_file_read_success(self): + self.runtime.stop() + + # Accept EULA set to true + eula_settings = { + "AcceptEULAForAllPatches": True, + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, True) + runtime.stop() + + # Accept EULA set to false + eula_settings = { + "AcceptEULAForAllPatches": False, + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + runtime.stop() + + # Accept EULA set to true in a string i.e. 'true' + eula_settings = { + "AcceptEULAForAllPatches": 'true', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, True) + runtime.stop() + + # Accept EULA set to true in a string i.e. 'True' + eula_settings = { + "AcceptEULAForAllPatches": 'True', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, True) + runtime.stop() + + # Accept EULA set to true in a string i.e. 'False' + eula_settings = { + "AcceptEULAForAllPatches": 'False', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + runtime.stop() + + # Accept EULA set to true in a string i.e. 'false' + eula_settings = { + "AcceptEULAForAllPatches": 'false', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + runtime.stop() + + # Accept EULA set as '0' + eula_settings = { + "AcceptEULAForAllPatches": '0', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + runtime.stop() + + # Accept EULA set as 0 + eula_settings = { + "AcceptEULAForAllPatches": 0, + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + runtime.stop() + + # Accept EULA set as 1 + eula_settings = { + "AcceptEULAForAllPatches": 1, + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, True) + runtime.stop() + + # Accept EULA set as '1' + eula_settings = { + "AcceptEULAForAllPatches": '1', + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, True) + runtime.stop() + + def test_eula_acceptance_file_read_when_no_data_found(self): + self.runtime.stop() + + # EULA file does not exist + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + self.assertFalse(os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS)) + runtime.stop() + + # EULA settings set to None + eula_settings = None + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + self.assertTrue(os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS)) + runtime.stop() + + # AcceptEULAForAllPatches not set in config + eula_settings = { + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + self.assertTrue(os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS)) + runtime.stop() + + # AcceptEULAForAllPatches not set to a boolean + eula_settings = { + "AcceptEULAForAllPatches": "test", + "AcceptedBy": "TestSetup", + "LastModified": "2023-08-29" + } + f = open(Constants.AzGPSPaths.EULA_SETTINGS, "w+") + f.write(json.dumps(eula_settings)) + f.close() + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + container = runtime.container + execution_config = container.get('execution_config') + self.assertEqual(execution_config.accept_package_eula, False) + self.assertTrue(os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS)) + runtime.stop() + + # EULA not accepted for cases where file read raises an Exception + runtime = RuntimeCompositor(self.argument_composer, True, package_manager_name=Constants.APT) + self.backup_read_with_retry = runtime.env_layer.file_system.read_with_retry + runtime.env_layer.file_system.read_with_retry = self.mock_read_with_retry_raise_exception + exec_config = ExecutionConfig(runtime.env_layer, runtime.composite_logger, str(self.argument_composer)) + self.assertTrue(os.path.exists(Constants.AzGPSPaths.EULA_SETTINGS)) + self.assertEqual(exec_config.accept_package_eula, False) + runtime.stop() + if __name__ == '__main__': unittest.main() diff --git a/src/core/tests/Test_ConfigurePatchingProcessor.py b/src/core/tests/Test_ConfigurePatchingProcessor.py index 2aa0a110..f36c884c 100644 --- a/src/core/tests/Test_ConfigurePatchingProcessor.py +++ b/src/core/tests/Test_ConfigurePatchingProcessor.py @@ -35,7 +35,16 @@ def tearDown(self): # self.runtime.stop() pass - def test_operation_success_for_configure_patching_request_for_apt(self): + #region Mocks + def mock_package_manager_get_current_auto_os_patch_state_returns_unknown(self): + if self.mock_package_manager_get_current_auto_os_patch_state_returns_unknown_call_count == 0: + self.mock_package_manager_get_current_auto_os_patch_state_returns_unknown_call_count = 1 + return Constants.AutomaticOSPatchStates.DISABLED + else: + return Constants.AutomaticOSPatchStates.UNKNOWN + #endregion Mocks + + def test_operation_success_for_configure_patching_request_for_apt_with_default_updates_config(self): # create and adjust arguments argument_composer = ArgumentComposer() argument_composer.operation = Constants.CONFIGURE_PATCHING @@ -80,14 +89,28 @@ def test_operation_success_for_configure_patching_request_for_apt(self): # stop test runtime runtime.stop() - #region Mocks - def mock_package_manager_get_current_auto_os_patch_state_returns_unknown(self): - if self.mock_package_manager_get_current_auto_os_patch_state_returns_unknown_call_count == 0: - self.mock_package_manager_get_current_auto_os_patch_state_returns_unknown_call_count = 1 - return Constants.AutomaticOSPatchStates.DISABLED - else: - return Constants.AutomaticOSPatchStates.UNKNOWN - #endregion Mocks + def test_operation_success_for_configure_patching_request_for_apt_without_default_updates_config(self): + # default auto OS updates config file not found on the machine + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.CONFIGURE_PATCHING + argument_composer.patch_mode = Constants.PatchModes.AUTOMATIC_BY_PLATFORM + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT) + runtime.package_manager.get_current_auto_os_patch_state = runtime.backup_get_current_auto_os_patch_state + runtime.set_legacy_test_type('HappyPath') + CoreMain(argument_composer.get_composed_arguments()) + + # check telemetry events + self.__check_telemetry_events(runtime) + + # check status file + with runtime.env_layer.file_system.open(runtime.execution_config.status_file_path, 'r') as file_handle: + substatus_file_data = json.load(file_handle)[0]["status"]["substatus"] + self.assertEqual(len(substatus_file_data), 2) + self.assertTrue(substatus_file_data[0]["name"] == Constants.PATCH_ASSESSMENT_SUMMARY) # assessment is now part of the CP flow + self.assertTrue(substatus_file_data[0]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + self.assertTrue(substatus_file_data[1]["name"] == Constants.CONFIGURE_PATCHING_SUMMARY) + self.assertTrue(substatus_file_data[1]["status"].lower() == Constants.STATUS_SUCCESS.lower()) + runtime.stop() def test_operation_success_for_installation_request_with_configure_patching(self): argument_composer = ArgumentComposer() @@ -163,29 +186,6 @@ def test_operation_fail_for_configure_patching_telemetry_not_supported(self): self.assertTrue(substatus_file_data[0]["status"].lower() == Constants.STATUS_SUCCESS.lower()) runtime.stop() - def test_operation_fail_for_configure_patching_request_for_apt(self): - # default auto OS updates config file not found on the machine - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.CONFIGURE_PATCHING - argument_composer.patch_mode = Constants.PatchModes.AUTOMATIC_BY_PLATFORM - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT) - runtime.package_manager.get_current_auto_os_patch_state = runtime.backup_get_current_auto_os_patch_state - runtime.set_legacy_test_type('HappyPath') - CoreMain(argument_composer.get_composed_arguments()) - - # check telemetry events - self.__check_telemetry_events(runtime) - - # check status file - with runtime.env_layer.file_system.open(runtime.execution_config.status_file_path, 'r') as file_handle: - substatus_file_data = json.load(file_handle)[0]["status"]["substatus"] - self.assertEqual(len(substatus_file_data), 2) - self.assertTrue(substatus_file_data[0]["name"] == Constants.PATCH_ASSESSMENT_SUMMARY) # assessment is now part of the CP flow - self.assertTrue(substatus_file_data[0]["status"].lower() == Constants.STATUS_SUCCESS.lower()) - self.assertTrue(substatus_file_data[1]["name"] == Constants.CONFIGURE_PATCHING_SUMMARY) - self.assertTrue(substatus_file_data[1]["status"].lower() == Constants.STATUS_ERROR.lower()) - runtime.stop() - def test_patch_mode_set_failure_for_configure_patching(self): argument_composer = ArgumentComposer() argument_composer.operation = Constants.CONFIGURE_PATCHING diff --git a/src/core/tests/Test_StatusHandler.py b/src/core/tests/Test_StatusHandler.py index 75a5f7de..09e227db 100644 --- a/src/core/tests/Test_StatusHandler.py +++ b/src/core/tests/Test_StatusHandler.py @@ -14,6 +14,7 @@ # # Requires Python 2.7+ import datetime +import glob import json import os import unittest @@ -31,6 +32,9 @@ def setUp(self): def tearDown(self): self.runtime.stop() + def __mock_os_remove(self, file_to_remove): + raise Exception("File could not be deleted") + def test_set_package_assessment_status(self): # startedBy should be set to User in status for Assessment packages, package_versions = self.runtime.package_manager.get_all_updates() @@ -417,9 +421,20 @@ def test_if_status_file_resets_on_load_if_malformed(self): self.assertEqual(len(substatus_file_data["status"]["substatus"]), 0) self.runtime.env_layer.file_system.delete_files_from_dir(example_file1, "*.complete.status") - def test_if_complete_status_path_is_dir(self): + def test_if_complete_and_status_path_is_dir(self): + self.old_complete_status_path = self.runtime.execution_config.complete_status_file_path + self.runtime.execution_config.complete_status_file_path = self.runtime.execution_config.status_folder + self.runtime.status_handler.load_status_file_components(initial_load=True) + self.assertTrue(os.path.isfile(os.path.join(self.runtime.execution_config.status_folder, '1.complete.status'))) + + self.old_status_path = self.runtime.execution_config.status_file_path self.runtime.execution_config.status_file_path = self.runtime.execution_config.status_folder - self.assertRaises(Exception, self.runtime.status_handler.load_status_file_components(initial_load=True)) + self.runtime.status_handler.load_status_file_components(initial_load=True) + self.assertTrue(os.path.isfile(os.path.join(self.runtime.execution_config.status_folder, '1.status'))) + + # reset the status path + self.runtime.execution_config.complete_status_file_path = self.old_complete_status_path + self.runtime.execution_config.status_file_path = self.old_status_path def test_assessment_packages_map(self): patch_count_for_test = 5 @@ -520,6 +535,39 @@ def test_load_status_and_set_package_install_status(self): self.assertTrue('Critical' in str(json.loads(substatus_file_data["formattedMessage"]["message"])["patches"][2]["classifications"])) self.runtime.env_layer.file_system.delete_files_from_dir(self.runtime.status_handler.status_file_path, '*.complete.status') + def test_remove_old_complete_status_files(self): + """ Create dummy files in status folder and check if the complete_status_file_path is the latest file and delete those dummy files """ + file_path = self.runtime.execution_config.status_folder + for i in range(1, 15): + with open(os.path.join(file_path, str(i + 100) + '.complete.status'), 'w') as f: + f.write("test" + str(i)) + + packages, package_versions = self.runtime.package_manager.get_all_updates() + self.runtime.status_handler.set_package_assessment_status(packages, package_versions) + self.runtime.status_handler.load_status_file_components(initial_load=True) + + # remove 10 complete status files + count_status_files = glob.glob(os.path.join(file_path, '*.complete.status')) + self.assertEqual(10, len(count_status_files)) + self.assertTrue(os.path.isfile(self.runtime.execution_config.complete_status_file_path)) + self.runtime.env_layer.file_system.delete_files_from_dir(file_path, '*.complete.status') + self.assertFalse(os.path.isfile(os.path.join(file_path, '1.complete_status'))) + + def test_remove_old_complete_status_files_throws_exception(self): + file_path = self.runtime.execution_config.status_folder + for i in range(1, 16): + with open(os.path.join(file_path, str(i + 100) + '.complete.status'), 'w') as f: + f.write("test" + str(i)) + + self.backup_os_remove = os.remove + os.remove = self.__mock_os_remove + self.assertRaises(Exception, self.runtime.status_handler.load_status_file_components(initial_load=True)) + + # reset os.remove() mock and remove *complete.status files + os.remove = self.backup_os_remove + self.runtime.env_layer.file_system.delete_files_from_dir(file_path, '*.complete.status') + self.assertFalse(os.path.isfile(os.path.join(file_path, '1.complete_status'))) + # Setup functions to populate packages and versions for truncation def __set_up_packages_func(self, val): test_packages = [] diff --git a/src/core/tests/library/ArgumentComposer.py b/src/core/tests/library/ArgumentComposer.py index 2b79290c..550f225b 100644 --- a/src/core/tests/library/ArgumentComposer.py +++ b/src/core/tests/library/ArgumentComposer.py @@ -47,6 +47,7 @@ def __init__(self): self.__status_folder = self.__get_custom_folder(scratch_folder, self.__STATUS_FOLDER) self.events_folder = self.__get_custom_folder(self.__log_folder, self.__EVENTS_FOLDER) self.temp_folder = self.__get_custom_folder(scratch_folder, self.__TEMP_FOLDER) + Constants.AzGPSPaths.EULA_SETTINGS = os.path.join(scratch_folder, "patch.eula.settings") # config settings self.operation = Constants.INSTALLATION diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index f6b7aa66..dd27ab33 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -47,7 +47,7 @@ def __init__(self, argv=Constants.DEFAULT_UNSPECIFIED_VALUE, legacy_mode=False, os.environ[Constants.LPE_ENV_VARIABLE] = self.current_env self.argv = argv if argv != Constants.DEFAULT_UNSPECIFIED_VALUE else ArgumentComposer().get_composed_arguments() self.vm_cloud_type = vm_cloud_type - Constants.Paths.SYSTEMD_ROOT = os.getcwd() # mocking to pass a basic systemd check in Windows + Constants.SystemPaths.SYSTEMD_ROOT = os.getcwd() # mocking to pass a basic systemd check in Windows self.is_github_runner = os.getenv('RUNNER_TEMP', None) is not None if self.is_github_runner: diff --git a/src/extension/src/Constants.py b/src/extension/src/Constants.py index 35cf8f85..a9f57c84 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.47" + EXT_VERSION = "1.6.48" # Runtime environments TEST = 'Test' diff --git a/src/extension/src/manifest.xml b/src/extension/src/manifest.xml index 10b16b93..49476bc2 100644 --- a/src/extension/src/manifest.xml +++ b/src/extension/src/manifest.xml @@ -2,7 +2,7 @@ Microsoft.CPlat.Core LinuxPatchExtension - 1.6.47 + 1.6.48 VmRole