From 396e2e7b36fdf20f4868131e5bb04e4bfcfeef7d Mon Sep 17 00:00:00 2001 From: john feng Date: Sat, 9 Nov 2024 16:09:18 -0800 Subject: [PATCH 01/20] add unit test for check_sudo_status() --- src/core/src/bootstrap/Bootstrapper.py | 70 +++++++++++++-------- src/core/src/bootstrap/Constants.py | 2 + src/core/tests/Test_CoreMain.py | 33 ++++++++++ src/core/tests/library/RuntimeCompositor.py | 58 +++++++++++++---- 4 files changed, 123 insertions(+), 40 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 4e349f26..7ee3f8b8 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -19,6 +19,7 @@ import json import os import sys +import time from core.src.bootstrap.ConfigurationFactory import ConfigurationFactory from core.src.bootstrap.Constants import Constants from core.src.bootstrap.Container import Container @@ -147,31 +148,46 @@ def basic_environment_health_check(self): def check_sudo_status(self, raise_if_not_sudo=True): """ Checks if we can invoke sudo successfully. """ - try: - self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") - return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) - # output should look like either this (bad): - # [sudo] password for username: - # False - # or this (good): - # uid=0(root) gid=0(root) groups=0(root) - # True - - output_lines = output.splitlines() - if len(output_lines) < 2: - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) - - if output_lines[1] == "True": - return True - elif output_lines[1] == "False": - if raise_if_not_sudo: - raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) - return False - else: - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) - except Exception as exception: - self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + - "Exception details: " + str(exception)) - if raise_if_not_sudo: - raise + for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): + try: + self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") + return_code, output = self.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) + # output should look like either this (bad): + # [sudo] password for username: + # False + # or this (good): + # uid=0(root) gid=0(root) groups=0(root) + # True + + output_lines = output.splitlines() + if len(output_lines) < 2: + self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + + if output_lines[1] == "True": + self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + return True + elif output_lines[1] == "False": + if raise_if_not_sudo: + self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) + return False + else: + self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + except Exception as exception: + self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + + "Exception details: " + str(exception)) + if attempts == Constants.MAX_CHECK_SUDO_RETRY_COUNT: + self.composite_logger.log_debug("Sudo Check Failed, reached [MaxAttempts={0}]".format(str(attempts))) + if raise_if_not_sudo: + raise + return False + + self.composite_logger.log_error("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) + time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) + + def run_command_output(self, command, no_output=False, chk_err=True): + """ Runs a command and returns the output """ + return self.env_layer.run_command_output(command, no_output, chk_err) diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index 99d58939..b45ff2ae 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -220,6 +220,8 @@ class StatusTruncationConfig(EnumBackport): MAX_IMDS_CONNECTION_RETRY_COUNT = 5 MAX_ZYPPER_REPO_REFRESH_RETRY_COUNT = 5 MAX_COMPLETE_STATUS_FILES_TO_RETAIN = 10 + MAX_CHECK_SUDO_RETRY_COUNT = 6 + MAX_CHECK_SUDO_INTERVAL_IN_SEC = 300 class PackageBatchConfig(EnumBackport): # Batch Patching Parameters diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 06d14965..512c9b61 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1215,6 +1215,39 @@ def test_delete_temp_folder_contents_failure(self): os.remove = self.backup_os_remove runtime.stop() + def test_check_sudo_status_always_true_throw_exception(self): + # Test retry logic in check sudo status all attempts failed + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='Always_False') + + # Run check_sudo_status and expect an exception after all retries + with self.assertRaises(Exception) as context: + runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + + # Verify exception msg contains the expected failure text + self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) + + runtime.stop() + + def test_check_sudo_status_succeeds_on_third_attempt(self): + # Test retry logic in check sudo status after 2 failed attempts followed by success (true) + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='Retry_True') + + # Attempt to check sudo status, succeed (true) on the 3rd attempt + result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + + # Verify the result is success (True) + self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") + + # Verify 3 attempts were made + self.assertEqual(runtime.sudo_check_status_attempts, 3, "Expected exactly 3 attempts in check_sudo_status") + runtime.stop() + def __check_telemetry_events(self, runtime): all_events = os.listdir(runtime.telemetry_writer.events_folder_path) self.assertTrue(len(all_events) > 0) diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index 86dae841..dafcde2f 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -41,9 +41,11 @@ class RuntimeCompositor(object): - def __init__(self, argv=Constants.DEFAULT_UNSPECIFIED_VALUE, legacy_mode=False, package_manager_name=Constants.APT, vm_cloud_type=Constants.VMCloudType.AZURE): + def __init__(self, argv=Constants.DEFAULT_UNSPECIFIED_VALUE, legacy_mode=False, package_manager_name=Constants.APT, vm_cloud_type=Constants.VMCloudType.AZURE, set_mock_sudo_status='Always_True'): # Init data self.original_rm_start_reboot = None + self.set_mock_sudo_status = set_mock_sudo_status + self.sudo_check_status_attempts = 0 self.current_env = Constants.DEV os.environ[Constants.LPE_ENV_VARIABLE] = self.current_env self.argv = argv if argv != Constants.DEFAULT_UNSPECIFIED_VALUE else ArgumentComposer().get_composed_arguments() @@ -70,28 +72,31 @@ def mkdtemp_runner(): urlreq.urlopen = self.mock_urlopen # Adapted bootstrapper - bootstrapper = Bootstrapper(self.argv, capture_stdout=False) + self.bootstrapper = Bootstrapper(self.argv, capture_stdout=False) - # Overriding sudo status check - Bootstrapper.check_sudo_status = self.check_sudo_status + # Store the original check_sudo_status method to reset when needed in stop() + self.original_check_sudo_status = Bootstrapper.check_sudo_status + + # Overriding sudo status check based on mock configuration + self.apply_mock_check_sudo_status() # Reconfigure env layer for legacy mode tests - self.env_layer = bootstrapper.env_layer + self.env_layer = self.bootstrapper.env_layer if legacy_mode: self.legacy_env_layer_extensions = LegacyEnvLayerExtensions(package_manager_name) self.reconfigure_env_layer_to_legacy_mode() # Core components - self.container = bootstrapper.build_out_container() - self.file_logger = bootstrapper.file_logger - self.composite_logger = bootstrapper.composite_logger + self.container = self.bootstrapper.build_out_container() + self.file_logger = self.bootstrapper.file_logger + self.composite_logger = self.bootstrapper.composite_logger # re-initializing telemetry_writer, outside of Bootstrapper, to correctly set the env_layer configured for tests - self.telemetry_writer = TelemetryWriter(self.env_layer, self.composite_logger, bootstrapper.telemetry_writer.events_folder_path, bootstrapper.telemetry_supported) - bootstrapper.telemetry_writer = self.telemetry_writer - bootstrapper.composite_logger.telemetry_writer = self.telemetry_writer + self.telemetry_writer = TelemetryWriter(self.env_layer, self.composite_logger, self.bootstrapper.telemetry_writer.events_folder_path, self.bootstrapper.telemetry_supported) + self.bootstrapper.telemetry_writer = self.telemetry_writer + self.bootstrapper.composite_logger.telemetry_writer = self.telemetry_writer - self.lifecycle_manager, self.status_handler = bootstrapper.build_core_components(self.container) + self.lifecycle_manager, self.status_handler = self.bootstrapper.build_core_components(self.container) # Business logic components self.execution_config = self.container.get('execution_config') @@ -106,7 +111,7 @@ def mkdtemp_runner(): self.patch_assessor = self.container.get('patch_assessor') self.patch_installer = self.container.get('patch_installer') self.maintenance_window = self.container.get('maintenance_window') - self.vm_cloud_type = bootstrapper.configuration_factory.vm_cloud_type + self.vm_cloud_type = self.bootstrapper.configuration_factory.vm_cloud_type # Extension handler dependency self.write_ext_state_file(self.lifecycle_manager.ext_state_file_path, self.execution_config.sequence_number, datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), self.execution_config.operation) @@ -126,6 +131,7 @@ def mkdtemp_runner(): def stop(self): self.file_logger.close(message_at_close="") self.container.reset() + Bootstrapper.check_sudo_status = self.original_check_sudo_status @staticmethod def write_ext_state_file(path, sequence_number, achieve_enable_by, operation): @@ -170,9 +176,35 @@ def reconfigure_package_manager(self): def mock_sleep(self, seconds): pass + def apply_mock_check_sudo_status(self): + # Apply the mock based on the set_mock_sudo_status flag + if self.set_mock_sudo_status == "Always_True": + Bootstrapper.check_sudo_status = self.check_sudo_status + elif self.set_mock_sudo_status == 'Always_False': + self.bootstrapper.run_command_output = self.mock_failed_run_command_output + elif self.set_mock_sudo_status == 'Retry_True': + self.bootstrapper.run_command_output = self.mock_retry_run_command_output + def check_sudo_status(self, raise_if_not_sudo=True): return True + def mock_failed_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a failed sudo check status command output to test retry logic.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:\nFalse") + + def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): + """Mock 2 failed sudo check status attempts followed by a success on the 3rd attempt.""" + self.sudo_check_status_attempts += 1 + + # Mock failure on the first two attempts + if self.sudo_check_status_attempts <= 2: + return (1, "[sudo] password for user:\nFalse") + + # Mock success (True) on the 3rd attempt + elif self.sudo_check_status_attempts == 3: + return (0, "uid=0(root) gid=0(root) groups=0(root)\nTrue") + def get_current_auto_os_patch_state(self): return Constants.AutomaticOSPatchStates.DISABLED From c7c86c68e70c29eab2f6c9f290b89d1e49955e4b Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 21:08:04 -0800 Subject: [PATCH 02/20] add unit case for insufficient outputline and unexpected output --- src/core/tests/Test_CoreMain.py | 44 ++++++++++++++++++++- src/core/tests/library/RuntimeCompositor.py | 18 ++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 512c9b61..7c92f53a 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1215,8 +1215,19 @@ def test_delete_temp_folder_contents_failure(self): os.remove = self.backup_os_remove runtime.stop() - def test_check_sudo_status_always_true_throw_exception(self): - # Test retry logic in check sudo status all attempts failed + def test_check_sudo_status_all_attempts_failed(self): + # Set raise_if_not_sudo=False to test the `return False` all attempts failed + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='Always_False') + + result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) + self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") + runtime.stop() + + def test_check_sudo_status_throw_exception(self): + # Set raise_if_not_sudo=True to throw exception argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, @@ -1228,7 +1239,36 @@ def test_check_sudo_status_always_true_throw_exception(self): # Verify exception msg contains the expected failure text self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) + runtime.stop() + + def test_check_sudo_status_insufficient_output_lines(self): + # Set raise_if_not_sudo=True to throw exception + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='insufficient_output_lines') + + # Run check_sudo_status and expect an exception after all retries + with self.assertRaises(Exception) as context: + runtime.bootstrapper.check_sudo_status() + # Verify exception msg contains the expected failure text + self.assertTrue("Unexpected sudo check result" in str(context.exception)) + runtime.stop() + + def test_check_sudo_status_unexpected_output_lines(self): + # Set raise_if_not_sudo=True to throw exception + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='unexpected_output') + + # Run check_sudo_status and expect an exception after all retries + with self.assertRaises(Exception) as context: + runtime.bootstrapper.check_sudo_status() + + # Verify exception msg contains the expected failure text + self.assertTrue("Unexpected sudo check result" in str(context.exception)) runtime.stop() def test_check_sudo_status_succeeds_on_third_attempt(self): diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index dafcde2f..fd23551d 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -181,18 +181,32 @@ def apply_mock_check_sudo_status(self): if self.set_mock_sudo_status == "Always_True": Bootstrapper.check_sudo_status = self.check_sudo_status elif self.set_mock_sudo_status == 'Always_False': - self.bootstrapper.run_command_output = self.mock_failed_run_command_output + self.bootstrapper.run_command_output = self.mock_false_run_command_output + elif self.set_mock_sudo_status == "insufficient_output_lines": + self.bootstrapper.run_command_output = self.mock_unexpected_output_run_command_output + elif self.set_mock_sudo_status == "unexpected_output": + self.bootstrapper.run_command_output = self.mock_unexpected_output_run_command_output elif self.set_mock_sudo_status == 'Retry_True': self.bootstrapper.run_command_output = self.mock_retry_run_command_output def check_sudo_status(self, raise_if_not_sudo=True): return True - def mock_failed_run_command_output(self, command, no_output=False, chk_err=True): + def mock_false_run_command_output(self, command, no_output=False, chk_err=True): """Mock a failed sudo check status command output to test retry logic.""" # Mock failure to trigger retry logic in check_sudo_status return (1, "[sudo] password for user:\nFalse") + def mock_insufficient_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a insufficient output line in sudo check status command output.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:") + + def mock_unexpected_output_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a insufficient output line in sudo check status command output.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:\nUnexpectedOutput") + def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): """Mock 2 failed sudo check status attempts followed by a success on the 3rd attempt.""" self.sudo_check_status_attempts += 1 From 226f18bbb5b7f44813da9cb2aecb9fba3ce90bfd Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 21:22:57 -0800 Subject: [PATCH 03/20] fix mock_insufficient_run_command_output --- src/core/tests/Test_CoreMain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 7c92f53a..e9c0b48d 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1217,6 +1217,7 @@ def test_delete_temp_folder_contents_failure(self): def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed + Constants.MAX_CHECK_SUDO_RETRY_COUNT = 3 argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, From e16c1457361e1e1ecab4b675469d6bf9379a6881 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 21:36:04 -0800 Subject: [PATCH 04/20] fix mock_insufficient_run_command_output --- src/core/tests/Test_CoreMain.py | 1 + src/core/tests/library/RuntimeCompositor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index e9c0b48d..9b9e0661 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1244,6 +1244,7 @@ def test_check_sudo_status_throw_exception(self): def test_check_sudo_status_insufficient_output_lines(self): # Set raise_if_not_sudo=True to throw exception + Constants.MAX_CHECK_SUDO_RETRY_COUNT = 1 argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index fd23551d..6cf6cba1 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -183,7 +183,7 @@ def apply_mock_check_sudo_status(self): elif self.set_mock_sudo_status == 'Always_False': self.bootstrapper.run_command_output = self.mock_false_run_command_output elif self.set_mock_sudo_status == "insufficient_output_lines": - self.bootstrapper.run_command_output = self.mock_unexpected_output_run_command_output + self.bootstrapper.run_command_output = self.mock_insufficient_run_command_output elif self.set_mock_sudo_status == "unexpected_output": self.bootstrapper.run_command_output = self.mock_unexpected_output_run_command_output elif self.set_mock_sudo_status == 'Retry_True': From 9812ffb5570926943a299d6a88b8fa4f31c7eb47 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 21:43:05 -0800 Subject: [PATCH 05/20] reomve retry_attempt constant --- src/core/tests/Test_CoreMain.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 9b9e0661..6911a690 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1217,7 +1217,6 @@ def test_delete_temp_folder_contents_failure(self): def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed - Constants.MAX_CHECK_SUDO_RETRY_COUNT = 3 argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, @@ -1244,7 +1243,6 @@ def test_check_sudo_status_throw_exception(self): def test_check_sudo_status_insufficient_output_lines(self): # Set raise_if_not_sudo=True to throw exception - Constants.MAX_CHECK_SUDO_RETRY_COUNT = 1 argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, @@ -1302,4 +1300,5 @@ def __check_telemetry_events(self, runtime): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() + From e600c8688b5fd1c112e1a0b339a72d0ab3df62cb Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 21:59:06 -0800 Subject: [PATCH 06/20] refactor func description --- src/core/tests/Test_CoreMain.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 6911a690..bbc1dc61 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1226,6 +1226,21 @@ def test_check_sudo_status_all_attempts_failed(self): self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") runtime.stop() + def test_check_sudo_status_one_attempts_failed(self): + # Set raise_if_not_sudo=False to test the `return False` one attempts failed + Constants.MAX_CHECK_SUDO_RETRY_COUNT = 1 + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, + set_mock_sudo_status='Always_False') + + result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) + self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") + + # Reset + Constants.MAX_CHECK_SUDO_RETRY_COUNT = 6 + runtime.stop() + def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception argument_composer = ArgumentComposer() @@ -1242,7 +1257,7 @@ def test_check_sudo_status_throw_exception(self): runtime.stop() def test_check_sudo_status_insufficient_output_lines(self): - # Set raise_if_not_sudo=True to throw exception + # Test insufficient output lines to raise exception argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, @@ -1257,7 +1272,7 @@ def test_check_sudo_status_insufficient_output_lines(self): runtime.stop() def test_check_sudo_status_unexpected_output_lines(self): - # Set raise_if_not_sudo=True to throw exception + # Test unexpected output neither false or true to raise exception argument_composer = ArgumentComposer() argument_composer.operation = Constants.ASSESSMENT runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, From 0a492378364717db4d6d1f7c20e88f6cb33735c8 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 10 Nov 2024 22:08:56 -0800 Subject: [PATCH 07/20] remove eol in test_coremain.py --- src/core/tests/Test_CoreMain.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index bbc1dc61..a03a5369 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1226,21 +1226,6 @@ def test_check_sudo_status_all_attempts_failed(self): self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") runtime.stop() - def test_check_sudo_status_one_attempts_failed(self): - # Set raise_if_not_sudo=False to test the `return False` one attempts failed - Constants.MAX_CHECK_SUDO_RETRY_COUNT = 1 - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='Always_False') - - result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) - self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") - - # Reset - Constants.MAX_CHECK_SUDO_RETRY_COUNT = 6 - runtime.stop() - def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception argument_composer = ArgumentComposer() @@ -1315,5 +1300,4 @@ def __check_telemetry_events(self, runtime): if __name__ == '__main__': - unittest.main() - + unittest.main() \ No newline at end of file From f71559f8e41754cb246cfff2db5dfd04e4049c51 Mon Sep 17 00:00:00 2001 From: john feng Date: Thu, 14 Nov 2024 09:40:40 -0800 Subject: [PATCH 08/20] mock env_layer instead --- src/core/src/bootstrap/Bootstrapper.py | 8 +------- src/core/tests/Test_CoreMain.py | 2 +- src/core/tests/library/RuntimeCompositor.py | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 7ee3f8b8..c91ea206 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -151,14 +151,13 @@ def check_sudo_status(self, raise_if_not_sudo=True): for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") - return_code, output = self.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) + return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) # output should look like either this (bad): # [sudo] password for username: # False # or this (good): # uid=0(root) gid=0(root) groups=0(root) # True - output_lines = output.splitlines() if len(output_lines) < 2: self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) @@ -186,8 +185,3 @@ def check_sudo_status(self, raise_if_not_sudo=True): self.composite_logger.log_error("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) - - def run_command_output(self, command, no_output=False, chk_err=True): - """ Runs a command and returns the output """ - return self.env_layer.run_command_output(command, no_output, chk_err) - diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index a03a5369..191b8b52 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1285,7 +1285,7 @@ def test_check_sudo_status_succeeds_on_third_attempt(self): self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") # Verify 3 attempts were made - self.assertEqual(runtime.sudo_check_status_attempts, 3, "Expected exactly 3 attempts in check_sudo_status") + self.assertEqual(runtime.sudo_check_status_attempts, 3, "Expected exactly 4 attempts in check_sudo_status") runtime.stop() def __check_telemetry_events(self, runtime): diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index 6cf6cba1..c5029241 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -77,15 +77,15 @@ def mkdtemp_runner(): # Store the original check_sudo_status method to reset when needed in stop() self.original_check_sudo_status = Bootstrapper.check_sudo_status - # Overriding sudo status check based on mock configuration - self.apply_mock_check_sudo_status() - # Reconfigure env layer for legacy mode tests self.env_layer = self.bootstrapper.env_layer if legacy_mode: self.legacy_env_layer_extensions = LegacyEnvLayerExtensions(package_manager_name) self.reconfigure_env_layer_to_legacy_mode() + # Overriding check_sudo_status and run_command_output based on mock configuration + self.apply_mock_check_sudo_status() + # Core components self.container = self.bootstrapper.build_out_container() self.file_logger = self.bootstrapper.file_logger @@ -128,10 +128,14 @@ def mkdtemp_runner(): self.backup_remove_timer = self.configure_patching_processor.auto_assess_timer_manager.remove_timer self.configure_patching_processor.auto_assess_timer_manager.remove_timer = self.mock_remove_timer + # Reset attempts to ensure accurate tracking, as run_command_output is being called by multiple objects beyond Bootstrapper + self.sudo_check_status_attempts = 0 + def stop(self): self.file_logger.close(message_at_close="") self.container.reset() Bootstrapper.check_sudo_status = self.original_check_sudo_status + self.sudo_check_status_attempts = 0 @staticmethod def write_ext_state_file(path, sequence_number, achieve_enable_by, operation): @@ -181,13 +185,13 @@ def apply_mock_check_sudo_status(self): if self.set_mock_sudo_status == "Always_True": Bootstrapper.check_sudo_status = self.check_sudo_status elif self.set_mock_sudo_status == 'Always_False': - self.bootstrapper.run_command_output = self.mock_false_run_command_output + self.env_layer.run_command_output = self.mock_false_run_command_output elif self.set_mock_sudo_status == "insufficient_output_lines": - self.bootstrapper.run_command_output = self.mock_insufficient_run_command_output + self.env_layer.run_command_output = self.mock_insufficient_run_command_output elif self.set_mock_sudo_status == "unexpected_output": - self.bootstrapper.run_command_output = self.mock_unexpected_output_run_command_output + self.env_layer.run_command_output = self.mock_unexpected_output_run_command_output elif self.set_mock_sudo_status == 'Retry_True': - self.bootstrapper.run_command_output = self.mock_retry_run_command_output + self.env_layer.run_command_output = self.mock_retry_run_command_output def check_sudo_status(self, raise_if_not_sudo=True): return True @@ -208,7 +212,7 @@ def mock_unexpected_output_run_command_output(self, command, no_output=False, ch return (1, "[sudo] password for user:\nUnexpectedOutput") def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): - """Mock 2 failed sudo check status attempts followed by a success on the 3rd attempt.""" + """Mock 3 failed sudo check status attempts followed by a success on the 4th attempt.""" self.sudo_check_status_attempts += 1 # Mock failure on the first two attempts From e4eb9b31f46edbae611bf54fbac6f7a7b162c347 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 17 Nov 2024 21:58:31 -0800 Subject: [PATCH 09/20] move mock check_sudo_check logic to new bootstrapper test file --- src/core/src/bootstrap/Constants.py | 1 + src/core/tests/Test_Bootstrapper.py | 116 ++++++++++++++++++++ src/core/tests/Test_CoreMain.py | 73 ------------ src/core/tests/library/RuntimeCompositor.py | 55 +--------- 4 files changed, 123 insertions(+), 122 deletions(-) create mode 100644 src/core/tests/Test_Bootstrapper.py diff --git a/src/core/src/bootstrap/Constants.py b/src/core/src/bootstrap/Constants.py index b45ff2ae..cd057746 100644 --- a/src/core/src/bootstrap/Constants.py +++ b/src/core/src/bootstrap/Constants.py @@ -220,6 +220,7 @@ class StatusTruncationConfig(EnumBackport): MAX_IMDS_CONNECTION_RETRY_COUNT = 5 MAX_ZYPPER_REPO_REFRESH_RETRY_COUNT = 5 MAX_COMPLETE_STATUS_FILES_TO_RETAIN = 10 + SET_CHECK_SUDO_STATUS_TRUE = True MAX_CHECK_SUDO_RETRY_COUNT = 6 MAX_CHECK_SUDO_INTERVAL_IN_SEC = 300 diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py new file mode 100644 index 00000000..08792159 --- /dev/null +++ b/src/core/tests/Test_Bootstrapper.py @@ -0,0 +1,116 @@ +# Copyright 2020 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.7+ +import unittest + +from core.src.bootstrap.Constants import Constants +from core.tests.library.ArgumentComposer import ArgumentComposer +from core.tests.library.RuntimeCompositor import RuntimeCompositor + + +class TestBootstrapper(unittest.TestCase): + def setUp(self): + self.sudo_check_status_attempts = 0 + Constants.SET_CHECK_SUDO_STATUS_TRUE = False + argument_composer = ArgumentComposer() + argument_composer.operation = Constants.ASSESSMENT + self.argv = argument_composer.get_composed_arguments() + self.runtime = RuntimeCompositor(self.argv, legacy_mode=True, package_manager_name=Constants.APT) + + def tearDown(self): + self.sudo_check_status_attempts = 0 + Constants.SET_CHECK_SUDO_STATUS_TRUE = True + self.runtime.stop() + + # regions mock + def mock_false_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a failed sudo check status command output to test retry logic.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:\nFalse") + + def mock_insufficient_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a insufficient output line in sudo check status command output.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:") + + def mock_unexpected_output_run_command_output(self, command, no_output=False, chk_err=True): + """Mock a insufficient output line in sudo check status command output.""" + # Mock failure to trigger retry logic in check_sudo_status + return (1, "[sudo] password for user:\nUnexpectedOutput") + + def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): + """Mock 3 failed sudo check status attempts followed by a success on the 4th attempt.""" + self.sudo_check_status_attempts += 1 + + # Mock failure on the first two attempts + if self.sudo_check_status_attempts <= 2: + return (1, "[sudo] password for user:\nFalse") + + # Mock success (True) on the 3rd attempt + elif self.sudo_check_status_attempts == 3: + return (0, "uid=0(root) gid=0(root) groups=0(root)\nTrue") + # end regions mock + + def test_check_sudo_status_all_attempts_failed(self): + # Set raise_if_not_sudo=False to test the `return False` all attempts failed + self.runtime.env_layer.run_command_output = self.mock_false_run_command_output + result = self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) + self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") + + def test_check_sudo_status_throw_exception(self): + # Set raise_if_not_sudo=True to throw exception) after all retries + self.runtime.env_layer.run_command_output = self.mock_false_run_command_output + with self.assertRaises(Exception) as context: + self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + + # Verify exception msg contains the expected failure text + self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) + + def test_check_sudo_status_insufficient_output_lines(self): + # Test insufficient output lines to raise exception after all retries + self.runtime.env_layer.run_command_output = self.mock_insufficient_run_command_output + + with self.assertRaises(Exception) as context: + self.runtime.bootstrapper.check_sudo_status() + + # Verify exception msg contains the expected failure text + self.assertTrue("Unexpected sudo check result" in str(context.exception)) + + def test_check_sudo_status_unexpected_output_lines(self): + # Test unexpected output with neither false or true to raise exception after all retries + self.runtime.env_layer.run_command_output = self.mock_unexpected_output_run_command_output + + with self.assertRaises(Exception) as context: + self.runtime.bootstrapper.check_sudo_status() + + # Verify exception msg contains the expected failure text + self.assertTrue("Unexpected sudo check result" in str(context.exception)) + + def test_check_sudo_status_succeeds_on_third_attempt(self): + # Test retry logic in check sudo status after 2 failed attempts followed by success (true) + self.runtime.env_layer.run_command_output = self.mock_retry_run_command_output + + # Attempt to check sudo status, succeed (true) on the 3rd attempt + result = self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + + # Verify the result is success (True) + self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") + + # Verify 3 attempts were made + self.assertEqual(self.sudo_check_status_attempts, 3, "Expected exactly 3 attempts in check_sudo_status") + + +if __name__ == '__main__': + unittest.main() diff --git a/src/core/tests/Test_CoreMain.py b/src/core/tests/Test_CoreMain.py index 191b8b52..06d14965 100644 --- a/src/core/tests/Test_CoreMain.py +++ b/src/core/tests/Test_CoreMain.py @@ -1215,79 +1215,6 @@ def test_delete_temp_folder_contents_failure(self): os.remove = self.backup_os_remove runtime.stop() - def test_check_sudo_status_all_attempts_failed(self): - # Set raise_if_not_sudo=False to test the `return False` all attempts failed - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='Always_False') - - result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) - self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") - runtime.stop() - - def test_check_sudo_status_throw_exception(self): - # Set raise_if_not_sudo=True to throw exception - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='Always_False') - - # Run check_sudo_status and expect an exception after all retries - with self.assertRaises(Exception) as context: - runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) - - # Verify exception msg contains the expected failure text - self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) - runtime.stop() - - def test_check_sudo_status_insufficient_output_lines(self): - # Test insufficient output lines to raise exception - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='insufficient_output_lines') - - # Run check_sudo_status and expect an exception after all retries - with self.assertRaises(Exception) as context: - runtime.bootstrapper.check_sudo_status() - - # Verify exception msg contains the expected failure text - self.assertTrue("Unexpected sudo check result" in str(context.exception)) - runtime.stop() - - def test_check_sudo_status_unexpected_output_lines(self): - # Test unexpected output neither false or true to raise exception - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='unexpected_output') - - # Run check_sudo_status and expect an exception after all retries - with self.assertRaises(Exception) as context: - runtime.bootstrapper.check_sudo_status() - - # Verify exception msg contains the expected failure text - self.assertTrue("Unexpected sudo check result" in str(context.exception)) - runtime.stop() - - def test_check_sudo_status_succeeds_on_third_attempt(self): - # Test retry logic in check sudo status after 2 failed attempts followed by success (true) - argument_composer = ArgumentComposer() - argument_composer.operation = Constants.ASSESSMENT - runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), legacy_mode=True, package_manager_name=Constants.APT, - set_mock_sudo_status='Retry_True') - - # Attempt to check sudo status, succeed (true) on the 3rd attempt - result = runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) - - # Verify the result is success (True) - self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") - - # Verify 3 attempts were made - self.assertEqual(runtime.sudo_check_status_attempts, 3, "Expected exactly 4 attempts in check_sudo_status") - runtime.stop() - def __check_telemetry_events(self, runtime): all_events = os.listdir(runtime.telemetry_writer.events_folder_path) self.assertTrue(len(all_events) > 0) diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index c5029241..a55e54e1 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -22,6 +22,8 @@ import time import uuid +from adodbapi import connect + from core.src.service_interfaces.TelemetryWriter import TelemetryWriter from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions @@ -41,11 +43,9 @@ class RuntimeCompositor(object): - def __init__(self, argv=Constants.DEFAULT_UNSPECIFIED_VALUE, legacy_mode=False, package_manager_name=Constants.APT, vm_cloud_type=Constants.VMCloudType.AZURE, set_mock_sudo_status='Always_True'): + def __init__(self, argv=Constants.DEFAULT_UNSPECIFIED_VALUE, legacy_mode=False, package_manager_name=Constants.APT, vm_cloud_type=Constants.VMCloudType.AZURE): # Init data self.original_rm_start_reboot = None - self.set_mock_sudo_status = set_mock_sudo_status - self.sudo_check_status_attempts = 0 self.current_env = Constants.DEV os.environ[Constants.LPE_ENV_VARIABLE] = self.current_env self.argv = argv if argv != Constants.DEFAULT_UNSPECIFIED_VALUE else ArgumentComposer().get_composed_arguments() @@ -83,8 +83,9 @@ def mkdtemp_runner(): self.legacy_env_layer_extensions = LegacyEnvLayerExtensions(package_manager_name) self.reconfigure_env_layer_to_legacy_mode() - # Overriding check_sudo_status and run_command_output based on mock configuration - self.apply_mock_check_sudo_status() + # Overriding check_sudo_status to always true + if Constants.SET_CHECK_SUDO_STATUS_TRUE: + Bootstrapper.check_sudo_status = self.check_sudo_status # Core components self.container = self.bootstrapper.build_out_container() @@ -128,14 +129,10 @@ def mkdtemp_runner(): self.backup_remove_timer = self.configure_patching_processor.auto_assess_timer_manager.remove_timer self.configure_patching_processor.auto_assess_timer_manager.remove_timer = self.mock_remove_timer - # Reset attempts to ensure accurate tracking, as run_command_output is being called by multiple objects beyond Bootstrapper - self.sudo_check_status_attempts = 0 - def stop(self): self.file_logger.close(message_at_close="") self.container.reset() Bootstrapper.check_sudo_status = self.original_check_sudo_status - self.sudo_check_status_attempts = 0 @staticmethod def write_ext_state_file(path, sequence_number, achieve_enable_by, operation): @@ -180,49 +177,9 @@ def reconfigure_package_manager(self): def mock_sleep(self, seconds): pass - def apply_mock_check_sudo_status(self): - # Apply the mock based on the set_mock_sudo_status flag - if self.set_mock_sudo_status == "Always_True": - Bootstrapper.check_sudo_status = self.check_sudo_status - elif self.set_mock_sudo_status == 'Always_False': - self.env_layer.run_command_output = self.mock_false_run_command_output - elif self.set_mock_sudo_status == "insufficient_output_lines": - self.env_layer.run_command_output = self.mock_insufficient_run_command_output - elif self.set_mock_sudo_status == "unexpected_output": - self.env_layer.run_command_output = self.mock_unexpected_output_run_command_output - elif self.set_mock_sudo_status == 'Retry_True': - self.env_layer.run_command_output = self.mock_retry_run_command_output - def check_sudo_status(self, raise_if_not_sudo=True): return True - def mock_false_run_command_output(self, command, no_output=False, chk_err=True): - """Mock a failed sudo check status command output to test retry logic.""" - # Mock failure to trigger retry logic in check_sudo_status - return (1, "[sudo] password for user:\nFalse") - - def mock_insufficient_run_command_output(self, command, no_output=False, chk_err=True): - """Mock a insufficient output line in sudo check status command output.""" - # Mock failure to trigger retry logic in check_sudo_status - return (1, "[sudo] password for user:") - - def mock_unexpected_output_run_command_output(self, command, no_output=False, chk_err=True): - """Mock a insufficient output line in sudo check status command output.""" - # Mock failure to trigger retry logic in check_sudo_status - return (1, "[sudo] password for user:\nUnexpectedOutput") - - def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): - """Mock 3 failed sudo check status attempts followed by a success on the 4th attempt.""" - self.sudo_check_status_attempts += 1 - - # Mock failure on the first two attempts - if self.sudo_check_status_attempts <= 2: - return (1, "[sudo] password for user:\nFalse") - - # Mock success (True) on the 3rd attempt - elif self.sudo_check_status_attempts == 3: - return (0, "uid=0(root) gid=0(root) groups=0(root)\nTrue") - def get_current_auto_os_patch_state(self): return Constants.AutomaticOSPatchStates.DISABLED From 02fd359f02825d44b56df56416defc465ce239b6 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 17 Nov 2024 22:15:03 -0800 Subject: [PATCH 10/20] remove the import adodbapi --- src/core/tests/library/RuntimeCompositor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/tests/library/RuntimeCompositor.py b/src/core/tests/library/RuntimeCompositor.py index a55e54e1..89629fda 100644 --- a/src/core/tests/library/RuntimeCompositor.py +++ b/src/core/tests/library/RuntimeCompositor.py @@ -22,8 +22,6 @@ import time import uuid -from adodbapi import connect - from core.src.service_interfaces.TelemetryWriter import TelemetryWriter from core.tests.library.ArgumentComposer import ArgumentComposer from core.tests.library.LegacyEnvLayerExtensions import LegacyEnvLayerExtensions From de4e261ac41c38408aa74c90203797a26306602d Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 17 Nov 2024 22:41:19 -0800 Subject: [PATCH 11/20] refactor Bootstrapper --- src/core/src/bootstrap/Bootstrapper.py | 1 + src/core/tests/Test_Bootstrapper.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index c91ea206..f1571cd3 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -158,6 +158,7 @@ def check_sudo_status(self, raise_if_not_sudo=True): # or this (good): # uid=0(root) gid=0(root) groups=0(root) # True + output_lines = output.splitlines() if len(output_lines) < 2: self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index 08792159..edc6b276 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -114,3 +114,4 @@ def test_check_sudo_status_succeeds_on_third_attempt(self): if __name__ == '__main__': unittest.main() + From d3b3d30039e77055d316647aceab640f79f3c855 Mon Sep 17 00:00:00 2001 From: john feng Date: Sun, 17 Nov 2024 23:11:47 -0800 Subject: [PATCH 12/20] move check_sudo_status into a retry method --- src/core/src/bootstrap/Bootstrapper.py | 58 +++++++++++++------------- src/core/tests/Test_Bootstrapper.py | 11 +++-- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index f1571cd3..8b437f12 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -143,46 +143,46 @@ def basic_environment_health_check(self): self.composite_logger.log("Process id: " + str(os.getpid())) # Ensure sudo works in the environment - sudo_check_result = self.check_sudo_status() + sudo_check_result = self.check_sudo_status_retry() self.composite_logger.log_debug("Sudo status check: " + str(sudo_check_result) + "\n") - def check_sudo_status(self, raise_if_not_sudo=True): - """ Checks if we can invoke sudo successfully. """ + def check_sudo_status_retry(self, raise_if_not_sudo=True): + """ retry to invoke sudo check """ for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: - self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") - return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) - # output should look like either this (bad): - # [sudo] password for username: - # False - # or this (good): - # uid=0(root) gid=0(root) groups=0(root) - # True - - output_lines = output.splitlines() - if len(output_lines) < 2: - self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) - - if output_lines[1] == "True": + self.composite_logger.log_debug("Performing sudo status check... This should complete within 10 seconds. [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + if self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo): self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) return True - elif output_lines[1] == "False": - if raise_if_not_sudo: - self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) - return False - else: - self.composite_logger.log_debug("Sudo Check Failed [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) except Exception as exception: - self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + - "Exception details: " + str(exception)) + self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + "Exception details: " + str(exception)) + if attempts == Constants.MAX_CHECK_SUDO_RETRY_COUNT: self.composite_logger.log_debug("Sudo Check Failed, reached [MaxAttempts={0}]".format(str(attempts))) if raise_if_not_sudo: raise - return False self.composite_logger.log_error("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) + + def check_sudo_status(self, raise_if_not_sudo=True): + """ Checks if we can invoke sudo successfully. """ + return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) + # output should look like either this (bad): + # [sudo] password for username: + # False + # or this (good): + # uid=0(root) gid=0(root) groups=0(root) + # True + output_lines = output.splitlines() + if len(output_lines) < 2: + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + + if output_lines[1] == "True": + return True + elif output_lines[1] == "False": + if raise_if_not_sudo: + raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) + return False + else: + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index edc6b276..dd4a65fd 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -66,14 +66,14 @@ def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed self.runtime.env_layer.run_command_output = self.mock_false_run_command_output - result = self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=False) + result = self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=False) self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception) after all retries self.runtime.env_layer.run_command_output = self.mock_false_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=True) # Verify exception msg contains the expected failure text self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) @@ -83,7 +83,7 @@ def test_check_sudo_status_insufficient_output_lines(self): self.runtime.env_layer.run_command_output = self.mock_insufficient_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status() + self.runtime.bootstrapper.check_sudo_status_retry() # Verify exception msg contains the expected failure text self.assertTrue("Unexpected sudo check result" in str(context.exception)) @@ -93,7 +93,7 @@ def test_check_sudo_status_unexpected_output_lines(self): self.runtime.env_layer.run_command_output = self.mock_unexpected_output_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status() + self.runtime.bootstrapper.check_sudo_status_retry() # Verify exception msg contains the expected failure text self.assertTrue("Unexpected sudo check result" in str(context.exception)) @@ -103,7 +103,7 @@ def test_check_sudo_status_succeeds_on_third_attempt(self): self.runtime.env_layer.run_command_output = self.mock_retry_run_command_output # Attempt to check sudo status, succeed (true) on the 3rd attempt - result = self.runtime.bootstrapper.check_sudo_status(raise_if_not_sudo=True) + result = self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=True) # Verify the result is success (True) self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") @@ -114,4 +114,3 @@ def test_check_sudo_status_succeeds_on_third_attempt(self): if __name__ == '__main__': unittest.main() - From 91ba35508d891037d464a4dcc3f1dac9bc47a807 Mon Sep 17 00:00:00 2001 From: john feng Date: Tue, 19 Nov 2024 12:43:29 -0800 Subject: [PATCH 13/20] undo check_sudo_status logic and add attempt checks in check_sudo_status_with_retry --- src/core/src/bootstrap/Bootstrapper.py | 58 ++++++++++++++------------ src/core/tests/Test_Bootstrapper.py | 10 ++--- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 8b437f12..c302bc48 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -143,46 +143,52 @@ def basic_environment_health_check(self): self.composite_logger.log("Process id: " + str(os.getpid())) # Ensure sudo works in the environment - sudo_check_result = self.check_sudo_status_retry() + sudo_check_result = self.check_sudo_status_with_retry() self.composite_logger.log_debug("Sudo status check: " + str(sudo_check_result) + "\n") - def check_sudo_status_retry(self, raise_if_not_sudo=True): + def check_sudo_status_with_retry(self, raise_if_not_sudo=True): """ retry to invoke sudo check """ for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: - self.composite_logger.log_debug("Performing sudo status check... This should complete within 10 seconds. [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + self.composite_logger.log_debug("Attempt sudo status check [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) if self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo): self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) return True except Exception as exception: - self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + "Exception details: " + str(exception)) - - if attempts == Constants.MAX_CHECK_SUDO_RETRY_COUNT: - self.composite_logger.log_debug("Sudo Check Failed, reached [MaxAttempts={0}]".format(str(attempts))) + if attempts >= 0 and attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: + self.composite_logger.log_error("Sudo Check Failed, reached [MaxAttempts={0}][Exception={1}]".format(str(attempts), str(exception))) if raise_if_not_sudo: raise - self.composite_logger.log_error("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) + self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) def check_sudo_status(self, raise_if_not_sudo=True): """ Checks if we can invoke sudo successfully. """ - return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) - # output should look like either this (bad): - # [sudo] password for username: - # False - # or this (good): - # uid=0(root) gid=0(root) groups=0(root) - # True - output_lines = output.splitlines() - if len(output_lines) < 2: - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) - - if output_lines[1] == "True": - return True - elif output_lines[1] == "False": + try: + self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") + return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) + # output should look like either this (bad): + # [sudo] password for username: + # False + # or this (good): + # uid=0(root) gid=0(root) groups=0(root) + # True + + output_lines = output.splitlines() + if len(output_lines) < 2: + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + + if output_lines[1] == "True": + return True + elif output_lines[1] == "False": + if raise_if_not_sudo: + raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) + return False + else: + raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + except Exception as exception: + self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + + "Exception details: " + str(exception)) if raise_if_not_sudo: - raise Exception("Unable to invoke sudo successfully. Output: " + " ".join(output.split("\n"))) - return False - else: - raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) + raise diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index dd4a65fd..cf512c3b 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -66,14 +66,14 @@ def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed self.runtime.env_layer.run_command_output = self.mock_false_run_command_output - result = self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=False) + result = self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=False) self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception) after all retries self.runtime.env_layer.run_command_output = self.mock_false_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=True) + self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=True) # Verify exception msg contains the expected failure text self.assertTrue("Unable to invoke sudo successfully" in str(context.exception)) @@ -83,7 +83,7 @@ def test_check_sudo_status_insufficient_output_lines(self): self.runtime.env_layer.run_command_output = self.mock_insufficient_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status_retry() + self.runtime.bootstrapper.check_sudo_status_with_retry() # Verify exception msg contains the expected failure text self.assertTrue("Unexpected sudo check result" in str(context.exception)) @@ -93,7 +93,7 @@ def test_check_sudo_status_unexpected_output_lines(self): self.runtime.env_layer.run_command_output = self.mock_unexpected_output_run_command_output with self.assertRaises(Exception) as context: - self.runtime.bootstrapper.check_sudo_status_retry() + self.runtime.bootstrapper.check_sudo_status_with_retry() # Verify exception msg contains the expected failure text self.assertTrue("Unexpected sudo check result" in str(context.exception)) @@ -103,7 +103,7 @@ def test_check_sudo_status_succeeds_on_third_attempt(self): self.runtime.env_layer.run_command_output = self.mock_retry_run_command_output # Attempt to check sudo status, succeed (true) on the 3rd attempt - result = self.runtime.bootstrapper.check_sudo_status_retry(raise_if_not_sudo=True) + result = self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=True) # Verify the result is success (True) self.assertTrue(result, "Expected check_sudo_status to succeed on the 3rd attempts") From 7a2159c8f33c426adac72cace4ccd0a9bb5080de Mon Sep 17 00:00:00 2001 From: john feng Date: Tue, 19 Nov 2024 13:17:13 -0800 Subject: [PATCH 14/20] change inner logerr to logdebug and move sudo status check log to retry --- src/core/src/bootstrap/Bootstrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index c302bc48..2f4159a8 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -150,7 +150,7 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): """ retry to invoke sudo check """ for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: - self.composite_logger.log_debug("Attempt sudo status check [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds. [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) if self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo): self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) return True @@ -166,7 +166,6 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): def check_sudo_status(self, raise_if_not_sudo=True): """ Checks if we can invoke sudo successfully. """ try: - self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) # output should look like either this (bad): # [sudo] password for username: @@ -188,7 +187,9 @@ def check_sudo_status(self, raise_if_not_sudo=True): else: raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n"))) except Exception as exception: - self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + + self.composite_logger.log_debug("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " + "Exception details: " + str(exception)) if raise_if_not_sudo: raise + + From 4060cc472aef62f6616382300c357ffc79e90ff5 Mon Sep 17 00:00:00 2001 From: john feng Date: Tue, 19 Nov 2024 13:41:01 -0800 Subject: [PATCH 15/20] fix rety try return --- src/core/src/bootstrap/Bootstrapper.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 2f4159a8..749ee978 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -147,15 +147,16 @@ def basic_environment_health_check(self): self.composite_logger.log_debug("Sudo status check: " + str(sudo_check_result) + "\n") def check_sudo_status_with_retry(self, raise_if_not_sudo=True): + # type:(bool) -> bool """ retry to invoke sudo check """ for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: - self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds. [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - if self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo): + sudo_status = self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo) + if sudo_status and attempts > 1: self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - return True + return sudo_status except Exception as exception: - if attempts >= 0 and attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: + if attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: self.composite_logger.log_error("Sudo Check Failed, reached [MaxAttempts={0}][Exception={1}]".format(str(attempts), str(exception))) if raise_if_not_sudo: raise @@ -166,6 +167,7 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): def check_sudo_status(self, raise_if_not_sudo=True): """ Checks if we can invoke sudo successfully. """ try: + self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") return_code, output = self.env_layer.run_command_output("timeout 10 sudo id && echo True || echo False", False, False) # output should look like either this (bad): # [sudo] password for username: @@ -191,5 +193,3 @@ def check_sudo_status(self, raise_if_not_sudo=True): "Exception details: " + str(exception)) if raise_if_not_sudo: raise - - From ee36fa4f0f8187fccd8761818b9a7bb20261c546 Mon Sep 17 00:00:00 2001 From: john feng Date: Tue, 19 Nov 2024 13:54:57 -0800 Subject: [PATCH 16/20] modify the sudo failed log and add line to eol and add data type to check_sudo_status --- src/core/src/bootstrap/Bootstrapper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 749ee978..13b8b3bf 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -153,18 +153,19 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): try: sudo_status = self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo) if sudo_status and attempts > 1: - self.composite_logger.log_debug("Sudo Check Successfully [Attempts={0}][MaxAttempts={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) + self.composite_logger.log_debug("Sudo Check Successfully [RetryCount={0}][MaxRetryCount={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) return sudo_status except Exception as exception: if attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: - self.composite_logger.log_error("Sudo Check Failed, reached [MaxAttempts={0}][Exception={1}]".format(str(attempts), str(exception))) + self.composite_logger.log_error("Customer environment error (sudo failure). [Exception={0}][RetryCount={1}]".format(str(exception), str(attempts))) if raise_if_not_sudo: raise - self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][Attempts={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) + self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][RetryCount={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) def check_sudo_status(self, raise_if_not_sudo=True): + # type:(bool) -> bool """ Checks if we can invoke sudo successfully. """ try: self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") @@ -193,3 +194,4 @@ def check_sudo_status(self, raise_if_not_sudo=True): "Exception details: " + str(exception)) if raise_if_not_sudo: raise + From c7e377fdb50c15405cd785e49b492ce048cc8852 Mon Sep 17 00:00:00 2001 From: John Feng Date: Fri, 22 Nov 2024 11:41:09 -0800 Subject: [PATCH 17/20] add check for None and return false retry when attempts exhausted --- src/core/src/bootstrap/Bootstrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index 13b8b3bf..e3743012 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -152,7 +152,7 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: sudo_status = self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo) - if sudo_status and attempts > 1: + if sudo_status is not None and sudo_status and attempts > 1: self.composite_logger.log_debug("Sudo Check Successfully [RetryCount={0}][MaxRetryCount={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) return sudo_status except Exception as exception: @@ -160,12 +160,12 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): self.composite_logger.log_error("Customer environment error (sudo failure). [Exception={0}][RetryCount={1}]".format(str(exception), str(attempts))) if raise_if_not_sudo: raise - + return False self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][RetryCount={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) def check_sudo_status(self, raise_if_not_sudo=True): - # type:(bool) -> bool + # type:(bool) -> any """ Checks if we can invoke sudo successfully. """ try: self.composite_logger.log("Performing sudo status check... This should complete within 10 seconds.") From 1fa1aa21c37d6a8d646bfc6a82ec1572f974a634 Mon Sep 17 00:00:00 2001 From: John Feng Date: Fri, 22 Nov 2024 14:58:23 -0800 Subject: [PATCH 18/20] add logic for handling sudo_status is false or none --- src/core/src/bootstrap/Bootstrapper.py | 18 +++++++++++++++--- src/core/tests/Test_Bootstrapper.py | 6 +++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index e3743012..e90d3cce 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -152,12 +152,24 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: sudo_status = self.check_sudo_status(raise_if_not_sudo=raise_if_not_sudo) - if sudo_status is not None and sudo_status and attempts > 1: + + if sudo_status and attempts > 1: self.composite_logger.log_debug("Sudo Check Successfully [RetryCount={0}][MaxRetryCount={1}]".format(str(attempts), Constants.MAX_CHECK_SUDO_RETRY_COUNT)) - return sudo_status + return sudo_status + + elif sudo_status is None or sudo_status is False: + if attempts < Constants.MAX_CHECK_SUDO_RETRY_COUNT: + self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][RetryCount={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) + time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) + continue + + elif attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: + self.composite_logger.log_error("Customer environment error (sudo failure).[MaxRetryCount={0}]".format( str(attempts))) + return False + except Exception as exception: if attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: - self.composite_logger.log_error("Customer environment error (sudo failure). [Exception={0}][RetryCount={1}]".format(str(exception), str(attempts))) + self.composite_logger.log_error("Customer environment error (sudo failure). [Exception={0}][MaxRetryCount={1}]".format(str(exception), str(attempts))) if raise_if_not_sudo: raise return False diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index cf512c3b..83b44bdd 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -41,12 +41,12 @@ def mock_false_run_command_output(self, command, no_output=False, chk_err=True): return (1, "[sudo] password for user:\nFalse") def mock_insufficient_run_command_output(self, command, no_output=False, chk_err=True): - """Mock a insufficient output line in sudo check status command output.""" + """Mock an insufficient output line in sudo check status command output.""" # Mock failure to trigger retry logic in check_sudo_status return (1, "[sudo] password for user:") def mock_unexpected_output_run_command_output(self, command, no_output=False, chk_err=True): - """Mock a insufficient output line in sudo check status command output.""" + """Mock an unexpected output line in sudo check status command output.""" # Mock failure to trigger retry logic in check_sudo_status return (1, "[sudo] password for user:\nUnexpectedOutput") @@ -67,7 +67,7 @@ def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed self.runtime.env_layer.run_command_output = self.mock_false_run_command_output result = self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=False) - self.assertFalse(result, "Expected check_sudo_status to return False after all attempts failed") + self.assertEqual(result, False, "Expected check_sudo_status retry to return False after all attempts failed") def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception) after all retries From e5928f43a4ce28477e622419ce65e8375ba178c7 Mon Sep 17 00:00:00 2001 From: John Feng Date: Fri, 22 Nov 2024 15:37:26 -0800 Subject: [PATCH 19/20] raise exception when sudo_status is None or false --- src/core/src/bootstrap/Bootstrapper.py | 3 +-- src/core/tests/Test_Bootstrapper.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index e90d3cce..ade54c88 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -164,8 +164,7 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): continue elif attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: - self.composite_logger.log_error("Customer environment error (sudo failure).[MaxRetryCount={0}]".format( str(attempts))) - return False + raise except Exception as exception: if attempts >= Constants.MAX_CHECK_SUDO_RETRY_COUNT: diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index 83b44bdd..9acc559c 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -66,7 +66,10 @@ def mock_retry_run_command_output(self, command, no_output=False, chk_err=True): def test_check_sudo_status_all_attempts_failed(self): # Set raise_if_not_sudo=False to test the `return False` all attempts failed self.runtime.env_layer.run_command_output = self.mock_false_run_command_output + result = self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=False) + + # Verify check_sudo_status_with_retry is False self.assertEqual(result, False, "Expected check_sudo_status retry to return False after all attempts failed") def test_check_sudo_status_throw_exception(self): From ba5660a611b6bc80b0481e3c4d15f4d77362dabc Mon Sep 17 00:00:00 2001 From: johnfeng Date: Fri, 22 Nov 2024 16:45:41 -0800 Subject: [PATCH 20/20] set retry to return none and update signature --- src/core/src/bootstrap/Bootstrapper.py | 3 +-- src/core/tests/Test_Bootstrapper.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/src/bootstrap/Bootstrapper.py b/src/core/src/bootstrap/Bootstrapper.py index ade54c88..50d66b03 100644 --- a/src/core/src/bootstrap/Bootstrapper.py +++ b/src/core/src/bootstrap/Bootstrapper.py @@ -147,7 +147,7 @@ def basic_environment_health_check(self): self.composite_logger.log_debug("Sudo status check: " + str(sudo_check_result) + "\n") def check_sudo_status_with_retry(self, raise_if_not_sudo=True): - # type:(bool) -> bool + # type:(bool) -> any """ retry to invoke sudo check """ for attempts in range(1, Constants.MAX_CHECK_SUDO_RETRY_COUNT + 1): try: @@ -171,7 +171,6 @@ def check_sudo_status_with_retry(self, raise_if_not_sudo=True): self.composite_logger.log_error("Customer environment error (sudo failure). [Exception={0}][MaxRetryCount={1}]".format(str(exception), str(attempts))) if raise_if_not_sudo: raise - return False self.composite_logger.log_debug("Retrying sudo status check after a delay of [ElapsedTimeInSeconds={0}][RetryCount={1}]".format(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC, str(attempts))) time.sleep(Constants.MAX_CHECK_SUDO_INTERVAL_IN_SEC) diff --git a/src/core/tests/Test_Bootstrapper.py b/src/core/tests/Test_Bootstrapper.py index 9acc559c..9df4859b 100644 --- a/src/core/tests/Test_Bootstrapper.py +++ b/src/core/tests/Test_Bootstrapper.py @@ -70,7 +70,7 @@ def test_check_sudo_status_all_attempts_failed(self): result = self.runtime.bootstrapper.check_sudo_status_with_retry(raise_if_not_sudo=False) # Verify check_sudo_status_with_retry is False - self.assertEqual(result, False, "Expected check_sudo_status retry to return False after all attempts failed") + self.assertEqual(result, None, "Expected check_sudo_status retry to return None after all attempts failed") def test_check_sudo_status_throw_exception(self): # Set raise_if_not_sudo=True to throw exception) after all retries